kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 2e4a28a4b42fbbc07eb68dd16b689b5bd1b77fcf
parent 971f78285dc58ebca6711d7cd4f04d8b56c7a5af
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 28 May 2026 20:33:43 -0700

Prune unreachable static AArch64 O1 functions

Diffstat:
Msrc/api/config_stubs.c | 8++++++++
Msrc/cg/ir.c | 30++++++++++++++++++++++++++++++
Msrc/cg/ir.h | 12++++++++++++
Msrc/cg/ir_recorder.c | 23+++++++++++++++++++++++
Msrc/debug/debug.c | 27+++++++++++++++++++++++++++
Msrc/debug/debug.h | 2++
Msrc/opt/opt.c | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/opt/static_prune_aa64.sh | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 10+++++++++-
9 files changed, 387 insertions(+), 1 deletion(-)

diff --git a/src/api/config_stubs.c b/src/api/config_stubs.c @@ -221,6 +221,11 @@ void debug_func_begin(Debug* d, ObjSymId sym, DebugTypeId fn_type, (void)decl; } +void debug_func_select(Debug* d, ObjSymId sym) { + (void)d; + (void)sym; +} + void debug_func_pc_range(Debug* d, ObjSecId text_section_id, u32 begin_ofs, u32 end_ofs) { (void)d; @@ -230,6 +235,9 @@ void debug_func_pc_range(Debug* d, ObjSecId text_section_id, u32 begin_ofs, } void debug_func_end(Debug* d) { (void)d; } + +void debug_prune_removed_funcs(Debug* d) { (void)d; } + void debug_scope_begin(Debug* d, SrcLoc loc) { (void)d; (void)loc; diff --git a/src/cg/ir.c b/src/cg/ir.c @@ -69,6 +69,36 @@ CgIrModule* cg_ir_module_new(Compiler* c) { return m; } +void cg_ir_symset_init_lazy(Compiler* c, ObjSymSet* s) { + if (!s || s->heap) return; + ObjSymSet_init_cap(s, c ? c->ctx->heap : NULL, 0); +} + +void cg_ir_symset_add(Compiler* c, ObjSymSet* s, ObjSymId sym) { + if (!s || sym == OBJ_SYM_NONE) return; + cg_ir_symset_init_lazy(c, s); + if (!s->heap) return; + (void)ObjSymSet_set(s, sym, 1); +} + +int cg_ir_symset_contains(const ObjSymSet* s, ObjSymId sym) { + return s && ObjSymSet_get(s, sym) != NULL; +} + +void cg_ir_symset_fini(ObjSymSet* s) { + if (s && s->heap) ObjSymSet_fini(s); +} + +void cg_ir_module_refsets_fini(CgIrModule* m) { + if (!m) return; + for (u32 i = 0; i < m->nfuncs; ++i) { + CgIrFunc* f = m->funcs[i]; + if (!f) continue; + cg_ir_symset_fini(&f->call_refs); + cg_ir_symset_fini(&f->global_refs); + } +} + void cg_ir_module_add_func(CgIrModule* m, CgIrFunc* f) { if (!m || !f) return; module_grow(m, m->nfuncs + 1u); diff --git a/src/cg/ir.h b/src/cg/ir.h @@ -3,6 +3,9 @@ #include "cg/cgtarget.h" #include "core/arena.h" +#include "core/hashmap.h" + +HASHMAP_DEFINE(ObjSymSet, ObjSymId, u8, hash_u32); typedef enum CgIrOp { CG_IR_NOP, @@ -216,6 +219,9 @@ typedef struct CgIrFunc { u32 nscopes; u32 scopes_cap; + ObjSymSet call_refs; + ObjSymSet global_refs; + u32 next_inst_id; u8 complete; u8 pad[3]; @@ -256,6 +262,12 @@ void cg_ir_module_add_func(CgIrModule*, CgIrFunc*); void cg_ir_module_add_alias(CgIrModule*, ObjSymId alias_sym, ObjSymId target_sym, CfreeCgTypeId type); void cg_ir_module_add_file_scope_asm(CgIrModule*, const char* src, size_t len); +void cg_ir_module_refsets_fini(CgIrModule*); + +void cg_ir_symset_init_lazy(Compiler*, ObjSymSet*); +void cg_ir_symset_add(Compiler*, ObjSymSet*, ObjSymId); +int cg_ir_symset_contains(const ObjSymSet*, ObjSymId); +void cg_ir_symset_fini(ObjSymSet*); CGLocal cg_ir_func_add_local(CgIrFunc*, const CGLocalDesc*, int is_param, u32 param_index); diff --git a/src/cg/ir_recorder.c b/src/cg/ir_recorder.c @@ -43,9 +43,21 @@ static CgIrInst* emit(CgIrRecorder* r, CgIrOp op) { return cg_ir_emit(require_func(r), op, r->loc); } +static void note_global_ref(CgIrRecorder* r, Operand op) { + if (op.kind == OPK_GLOBAL) + cg_ir_symset_add(r->base.c, &require_func(r)->global_refs, + op.v.global.sym); +} + +static void note_global_refs(CgIrRecorder* r, const Operand* ops, u32 n) { + if (!ops) return; + for (u32 i = 0; i < n; ++i) note_global_ref(r, ops[i]); +} + static void set_ops(CgIrRecorder* r, CgIrInst* in, const Operand* ops, u32 n) { in->opnds = cg_ir_dup_operands(require_func(r)->arena, ops, n); in->nopnds = n; + note_global_refs(r, ops, n); } static void rec_func_begin(CgTarget* t, const CGFuncDesc* desc) { @@ -312,6 +324,7 @@ static void rec_tls_addr_of(CgTarget* t, Operand dst, ObjSymId sym, aux->sym = sym; aux->addend = addend; set_ops(r, in, &dst, 1); + cg_ir_symset_add(r->base.c, &require_func(r)->global_refs, sym); in->extra.aux = aux; } @@ -393,6 +406,11 @@ static void rec_call(CgTarget* t, const CGCallDesc* desc) { CgIrInst* in = emit(r, CG_IR_CALL); CgIrCallAux* aux = AUX_NEW(r, CgIrCallAux); aux->desc = cg_ir_dup_call_desc(require_func(r)->arena, desc); + note_global_ref(r, desc->callee); + if (desc->callee.kind == OPK_GLOBAL && desc->callee.v.global.addend == 0) { + cg_ir_symset_add(r->base.c, &require_func(r)->call_refs, + desc->callee.v.global.sym); + } in->extra.aux = aux; } @@ -512,6 +530,8 @@ static void rec_intrinsic(CgTarget* t, IntrinKind kind, Operand* dsts, u32 ndst, aux->args = cg_ir_dup_operands(require_func(r)->arena, args, narg); aux->ndst = ndst; aux->narg = narg; + note_global_refs(r, dsts, ndst); + note_global_refs(r, args, narg); in->extra.aux = aux; } @@ -529,6 +549,8 @@ static void rec_asm_block(CgTarget* t, const char* tmpl, aux->out_ops = cg_ir_dup_operands(f->arena, out_ops, nout); aux->ins = cg_ir_dup_asm_constraints(f->arena, ins, nin); aux->in_ops = cg_ir_dup_operands(f->arena, in_ops, nin); + note_global_refs(r, out_ops, nout); + note_global_refs(r, in_ops, nin); if (nclob) { aux->clobbers = arena_array(f->arena, Sym, nclob); memcpy(aux->clobbers, clobbers, sizeof(*aux->clobbers) * nclob); @@ -555,6 +577,7 @@ static void rec_finalize(CgTarget* t) { static void rec_destroy(CgTarget* t) { CgIrRecorder* r = rec_of(t); if (r->destroy_user) r->destroy_user(r->user); + cg_ir_module_refsets_fini(r->module); } CgTarget* cg_ir_recorder_new(Compiler* c, ObjBuilder* obj, diff --git a/src/debug/debug.c b/src/debug/debug.c @@ -357,6 +357,16 @@ void debug_func_begin(Debug* d, ObjSymId sym, DebugTypeId fn_type, d->nfuncs++; } +void debug_func_select(Debug* d, ObjSymId sym) { + if (!d || sym == OBJ_SYM_NONE) return; + for (u32 i = 0; i < d->nfuncs; ++i) { + if (d->funcs[i].sym == sym) { + d->cur_func = (i32)i; + return; + } + } +} + void debug_func_pc_range(Debug* d, ObjSecId text_section, u32 begin_ofs, u32 end_ofs) { if (d->cur_func < 0) return; @@ -374,6 +384,23 @@ void debug_func_end(Debug* d) { d->cur_func = -1; } +void debug_prune_removed_funcs(Debug* d) { + u32 w = 0; + if (!d) return; + for (u32 r = 0; r < d->nfuncs; ++r) { + DebugFunc* f = &d->funcs[r]; + const ObjSym* sym = obj_symbol_get(d->ob, f->sym); + if (!sym || sym->removed) { + func_free(d, f); + continue; + } + if (w != r) d->funcs[w] = *f; + ++w; + } + d->nfuncs = w; + d->cur_func = -1; +} + /* ---- scopes ---- */ void debug_scope_begin(Debug* d, SrcLoc loc) { diff --git a/src/debug/debug.h b/src/debug/debug.h @@ -93,9 +93,11 @@ DebugTypeId debug_type_enum_end(DebugEnumBuilder*); * ============================================================ */ void debug_func_begin(Debug*, ObjSymId, DebugTypeId fn_type, SrcLoc decl); +void debug_func_select(Debug*, ObjSymId); void debug_func_pc_range(Debug*, ObjSecId text_section_id, u32 begin_ofs, u32 end_ofs); void debug_func_end(Debug*); +void debug_prune_removed_funcs(Debug*); /* lexical scopes (nested between func_begin/end) */ void debug_scope_begin(Debug*, SrcLoc); diff --git a/src/opt/opt.c b/src/opt/opt.c @@ -7,8 +7,10 @@ #include "cg/type.h" #include "core/arena.h" #include "core/core.h" +#include "core/hashmap.h" #include "core/metrics.h" #include "core/strbuf.h" +#include "debug/debug.h" #include "opt/opt_internal.h" #undef Operand @@ -33,6 +35,8 @@ typedef struct OptImpl { u32 cg_cap; } OptImpl; +HASHMAP_DEFINE(OptFuncIndex, ObjSymId, u32, hash_u32); + /* Lazily re-lower (and cache) the pre-machinize Func for a recorded callee * symbol. Returns NULL for forward-defined callees not yet recorded. */ static Func* opt_tiny_callee_lookup(void* ctx, ObjSymId sym) { @@ -210,7 +214,11 @@ static void opt_run_o1_native(OptImpl* o, Func* f) { opt_dbg_dump(o, f, "pre-emit"); metrics_scope_begin(o->c, "opt.emit"); + if (o->native->mc && o->native->mc->debug) + debug_func_select(o->native->mc->debug, f->desc.sym); opt_emit_native(o->c, f, o->native); + if (o->native->mc && o->native->mc->debug) + debug_func_end(o->native->mc->debug); metrics_scope_end(o->c, "opt.emit"); metrics_scope_end(o->c, "opt.o1.total"); } @@ -238,12 +246,180 @@ static void opt_on_func(void* user, CgIrFunc* cg_func) { /* The dump writer renders the semantic CG IR tape — the IR as recorded, * before lowering to the optimizer's CFG form. */ if (o->dump_writer) cg_ir_func_dump(cg_func, o->dump_writer); + if (o->c->target.arch == CFREE_ARCH_ARM_64) return; metrics_scope_begin(o->c, "opt.o1.cg_ir_lower"); f = opt_func_from_cg_ir(o->c, cg_func); metrics_scope_end(o->c, "opt.o1.cg_ir_lower"); opt_run_o1_native(o, f); } +static int opt_func_is_root(OptImpl* o, const CgIrFunc* f) { + const ObjSym* s; + if (!f || f->desc.sym == OBJ_SYM_NONE) return 0; + s = obj_symbol_get(o->target->obj, f->desc.sym); + if (!s || s->removed) return 0; + if (s->bind != SB_LOCAL) return 1; + if (s->flags & CFREE_CG_SYM_USED) return 1; + return 0; +} + +static void opt_mark_func(u8* reachable, u8* queued, u32* queue, u32* qtail, + u32 idx) { + if (reachable[idx]) return; + reachable[idx] = 1; + if (!queued[idx]) { + queued[idx] = 1; + queue[(*qtail)++] = idx; + } +} + +static void opt_mark_sym(OptFuncIndex* index, u8* reachable, u8* queued, + u32* queue, u32* qtail, ObjSymId sym) { + u32* slot; + if (sym == OBJ_SYM_NONE) return; + slot = OptFuncIndex_get(index, sym); + if (slot) opt_mark_func(reachable, queued, queue, qtail, *slot); +} + +static void opt_mark_symset(OptFuncIndex* index, u8* reachable, u8* queued, + u32* queue, u32* qtail, + const ObjSymSet* refs) { + if (!refs || !refs->cap) return; + for (u32 i = 0; i < refs->cap; ++i) { + ObjSymId sym = refs->slots[i].k; + if (sym != OBJ_SYM_NONE) + opt_mark_sym(index, reachable, queued, queue, qtail, sym); + } +} + +static int opt_data_reloc_is_exported_root(OptImpl* o, const Reloc* r) { + ObjSymIter* it; + ObjSymEntry ent; + const Section* sec; + if (!r || r->removed || r->section_id == OBJ_SEC_NONE) return 0; + sec = obj_section_get(o->target->obj, r->section_id); + if (!sec || sec->removed || sec->kind == SEC_TEXT) return 0; + if (sec->flags & SF_RETAIN) return 1; + it = obj_symiter_new(o->target->obj); + if (!it) return 0; + while (obj_symiter_next(it, &ent)) { + const ObjSym* s = ent.sym; + u64 begin, end; + if (!s || s->removed || s->section_id != r->section_id) continue; + if (s->kind != SK_OBJ && s->kind != SK_TLS && s->kind != SK_COMMON) + continue; + if (s->bind == SB_LOCAL && !(s->flags & CFREE_CG_SYM_USED)) continue; + begin = s->value; + end = begin + s->size; + if (s->size == 0) end = begin + 1u; + if ((u64)r->offset >= begin && (u64)r->offset < end) { + obj_symiter_free(it); + return 1; + } + } + obj_symiter_free(it); + return 0; +} + +static void opt_root_exported_data_relocs(OptImpl* o, OptFuncIndex* index, + u8* reachable, u8* queued, + u32* queue, u32* qtail) { + u32 nrel = obj_reloc_total(o->target->obj); + for (u32 i = 0; i < nrel; ++i) { + const Reloc* r = obj_reloc_at(o->target->obj, i); + if (opt_data_reloc_is_exported_root(o, r)) + opt_mark_sym(index, reachable, queued, queue, qtail, r->sym); + } +} + +static void opt_root_aliases(OptImpl* o, const CgIrModule* module, + OptFuncIndex* index, u8* reachable, u8* queued, + u32* queue, u32* qtail) { + for (u32 i = 0; module && i < module->naliases; ++i) { + const CgIrAlias* a = &module->aliases[i]; + const ObjSym* s = obj_symbol_get(o->target->obj, a->alias_sym); + if (!s || s->removed) continue; + if (s->bind != SB_LOCAL || (s->flags & CFREE_CG_SYM_USED)) + opt_mark_sym(index, reachable, queued, queue, qtail, a->target_sym); + } +} + +static void opt_refresh_or_prune_aliases(OptImpl* o, const CgIrModule* module, + OptFuncIndex* index, + const u8* reachable) { + for (u32 i = 0; module && i < module->naliases; ++i) { + const CgIrAlias* a = &module->aliases[i]; + const ObjSym* ts; + const ObjSym* as; + u32* target_idx = OptFuncIndex_get(index, a->target_sym); + if (!target_idx || !reachable[*target_idx]) { + as = obj_symbol_get(o->target->obj, a->alias_sym); + if (as && as->bind == SB_LOCAL) + obj_symbol_remove(o->target->obj, a->alias_sym); + continue; + } + ts = obj_symbol_get(o->target->obj, a->target_sym); + if (ts && !ts->removed && ts->section_id != OBJ_SEC_NONE) + obj_symbol_define(o->target->obj, a->alias_sym, ts->section_id, + ts->value, ts->size); + } +} + +static void opt_prune_debug(OptImpl* o) { + if (o->native && o->native->mc && o->native->mc->debug) + debug_prune_removed_funcs(o->native->mc->debug); +} + +static void opt_emit_reachable_aarch64(OptImpl* o, const CgIrModule* module) { + OptFuncIndex index; + u8* reachable; + u8* queued; + u32* queue; + u32 qhead = 0; + u32 qtail = 0; + if (!module || !module->nfuncs) return; + OptFuncIndex_init_cap(&index, o->c->ctx->heap, 0); + reachable = arena_zarray(o->c->tu, u8, module->nfuncs); + queued = arena_zarray(o->c->tu, u8, module->nfuncs); + queue = arena_array(o->c->tu, u32, module->nfuncs); + for (u32 i = 0; i < module->nfuncs; ++i) { + CgIrFunc* f = module->funcs[i]; + if (f && f->desc.sym != OBJ_SYM_NONE) + (void)OptFuncIndex_set(&index, f->desc.sym, i); + } + for (u32 i = 0; i < module->nfuncs; ++i) { + if (module->nfile_scope_asms || opt_func_is_root(o, module->funcs[i])) + opt_mark_func(reachable, queued, queue, &qtail, i); + } + opt_root_aliases(o, module, &index, reachable, queued, queue, &qtail); + opt_root_exported_data_relocs(o, &index, reachable, queued, queue, &qtail); + while (qhead < qtail) { + CgIrFunc* f = module->funcs[queue[qhead++]]; + opt_mark_symset(&index, reachable, queued, queue, &qtail, &f->call_refs); + opt_mark_symset(&index, reachable, queued, queue, &qtail, &f->global_refs); + } + for (u32 i = 0; i < module->nfuncs; ++i) { + CgIrFunc* cg_func = module->funcs[i]; + if (reachable[i]) continue; + if (cg_func && cg_func->desc.sym != OBJ_SYM_NONE) { + const ObjSym* s = obj_symbol_get(o->target->obj, cg_func->desc.sym); + if (s && s->bind == SB_LOCAL) + obj_symbol_remove(o->target->obj, cg_func->desc.sym); + } + } + opt_prune_debug(o); + for (u32 i = 0; i < module->nfuncs; ++i) { + Func* f; + if (!reachable[i]) continue; + metrics_scope_begin(o->c, "opt.o1.cg_ir_lower"); + f = opt_func_from_cg_ir(o->c, module->funcs[i]); + metrics_scope_end(o->c, "opt.o1.cg_ir_lower"); + opt_run_o1_native(o, f); + } + opt_refresh_or_prune_aliases(o, module, &index, reachable); + OptFuncIndex_fini(&index); +} + static void opt_on_finalize(void* user, const CgIrModule* module) { OptImpl* o = (OptImpl*)user; /* File-scope asm blocks are captured during recording (no live target then) @@ -254,6 +430,8 @@ static void opt_on_finalize(void* user, const CgIrModule* module) { o->native->file_scope_asm(o->native, module->file_scope_asms[i].src, module->file_scope_asms[i].len); } + if (o->c->target.arch == CFREE_ARCH_ARM_64) + opt_emit_reachable_aarch64(o, module); if (o->native && o->native->finalize) o->native->finalize(o->native); } diff --git a/test/opt/static_prune_aa64.sh b/test/opt/static_prune_aa64.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Structural checks for AArch64 O1 static-function pruning. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +CFREE="${CFREE:-$ROOT/build/cfree}" +WORK="$ROOT/build/test/opt/static_prune_aa64" +mkdir -p "$WORK" + +compile_case() { + local name=$1 + local src="$WORK/$name.c" + local obj="$WORK/$name.o" + cat > "$src" + "$CFREE" cc -target aarch64-linux-gnu -O1 -std=c11 -c "$src" \ + -o "$obj" > "$WORK/$name.cc.out" 2>&1 + "$CFREE" nm "$obj" > "$WORK/$name.nm" 2>&1 +} + +has_sym() { + local name=$1 + local sym=$2 + grep -Eq "[[:space:]]$sym$" "$WORK/$name.nm" +} + +want_sym() { + local name=$1 + local sym=$2 + if ! has_sym "$name" "$sym"; then + printf 'static-prune FAILED: %s missing expected symbol %s\n' \ + "$name" "$sym" >&2 + sed 's/^/ | /' "$WORK/$name.nm" >&2 + exit 1 + fi +} + +want_no_sym() { + local name=$1 + local sym=$2 + if has_sym "$name" "$sym"; then + printf 'static-prune FAILED: %s kept unexpected symbol %s\n' \ + "$name" "$sym" >&2 + sed 's/^/ | /' "$WORK/$name.nm" >&2 + exit 1 + fi +} + +compile_case unused_static <<'EOF' +static int dead_unused(void) { return 1; } +int exported_live(void) { return 2; } +EOF +want_no_sym unused_static dead_unused +want_sym unused_static exported_live + +compile_case reached_static <<'EOF' +static int reached_helper(void) { return 3; } +int exported_calls_helper(void) { return reached_helper(); } +EOF +want_sym reached_static reached_helper +want_sym reached_static exported_calls_helper + +compile_case call_chain <<'EOF' +static int chain_leaf(void) { return 4; } +static int chain_mid(void) { return chain_leaf(); } +int exported_chain(void) { return chain_mid(); } +EOF +want_sym call_chain chain_leaf +want_sym call_chain chain_mid + +compile_case global_fnptr <<'EOF' +static int pointed_target(void) { return 5; } +int (*exported_fp)(void) = pointed_target; +EOF +want_sym global_fnptr pointed_target +want_sym global_fnptr exported_fp + +compile_case used_attr <<'EOF' +static int used_target(void) __attribute__((used)); +static int used_target(void) { return 6; } +int exported_for_used_case(void) { return 0; } +EOF +want_sym used_attr used_target + +compile_case alias_root <<'EOF' +static int aliased_target(void) { return 7; } +int exported_alias(void) __attribute__((alias("aliased_target"))); +EOF +want_sym alias_root aliased_target +want_sym alias_root exported_alias + +compile_case file_scope_asm <<'EOF' +__asm__(".text\n"); +static int asm_conserved_local(void) { return 8; } +int exported_with_asm(void) { return 0; } +EOF +want_sym file_scope_asm asm_conserved_local + +printf 'static-prune-aa64: ok\n' diff --git a/test/test.mk b/test/test.mk @@ -223,6 +223,10 @@ $(DEBUG_TEST_BIN): test/debug/roundtrip_unit.c $(LIB_OBJS) test-dbg: bin @CFREE=$(abspath $(BIN)) sh test/dbg/run.sh +.PHONY: test-dbg-red +test-dbg-red: bin + @CFREE=$(abspath $(BIN)) DBG_STRICT_XFAIL=1 sh test/dbg/run.sh + # aa64 ISA descriptor-table unit test (doc/ASM.md phase 2). Covers # every AA64Format the table maps and the alias-precedence invariant # (first-match disasm picks the alias spelling over the canonical @@ -517,7 +521,7 @@ test-macho: lib $(TEST_RT_DEP) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_R OPT_TEST_BIN = build/test/cg_ir_lower_test TINY_INLINE_TEST_BIN = build/test/tiny_inline_test -test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg +test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 $(OPT_TEST_BIN) $(OPT_TEST_BIN): test/opt/cg_ir_lower_test.c $(LIB_OBJS) @@ -540,6 +544,10 @@ test-opt-inline: bin test-opt-zero-arg: bin @CFREE=$(abspath $(BIN)) bash test/opt/zero_arg.sh +.PHONY: test-opt-static-prune-aa64 +test-opt-static-prune-aa64: bin + @CFREE=$(abspath $(BIN)) bash test/opt/static_prune_aa64.sh + test-parse: test-parse-ok test-parse-err test-parse-ok: lib $(TEST_RT_DEP) $(PARSE_RUNNER) $(ROUNDTRIP_BIN) $(LINK_EXE_RUNNER) $(JIT_RUNNER)