/** * @file Codegen.cpp * @brief 寄存器码生成(配置驱动;compiler 不依赖 isa) * @author * @date 2026-08-21 * * @details 设计说明(详见 Doc/compiler/寄存器码.md 与 Doc/isa/指令配置.md): * - 指令 opcode / 类型 tag / FB 布局全部来自 machine.toml(MachineConfig) * - 帧/字面量/MOVE/RET;全局数据区;短路 AND/OR;CMP 与 IF;WHILE 与四则; * FUNCTION 与 CALL(调用约定 r0/r1..r7/r8+) * - FB 实例 → 数据区实例块(跨周期持久);字段槽号 = 实例基槽 + 字段序号 * - 用户 FB 调用点内联展开;内建 FB → CAL_* <实例基槽> * - 数据段每槽 8 字节定宽,指令 slot = 槽号 * * 函数清单: * - Builder::Builder (构造)存工程/源文件/链接结果/机器配置/输出,收集 io 绑定 * - Builder::run 布局数据区 → 逐 POU 建函数 → 拼映像 * - Builder::fail / opc 错误 / 按名查配置 opcode * - E_rr / E_rrr / E_imm / E_slot / E_jmp / E_jc / E_call / E_ret 指令发射(配置 opcode) * - find_pou / find_fn_id 按名查 POU AST / fn_id * - build_function 编译一个 POU(帧约定分配;FB 占位) * - begin_stmt / alloc_temp 临时寄存器管理 * - compile_stmt 语句编译(赋值 / IF / WHILE / FB 调用) * - compile_if / compile_while / compile_fb_call / compile_fb_inline * - store_target / compile_expr 左值存储 / 表达式编译 * - arith_name / cmp_name / load_name / store_name 操作码名选择 * - patch_jump 回填跳转偏移(相对下一条) * - layout_data / layout_fb_instances / instance_of / init_of * - const_id 取常量表 id(tag 来自配置类型表) * - assemble_image 拼头 + 常量表 + 函数表 + 字节码 + 数据段 * - codegen_project 对外入口 */ #include "compiler/Codegen.h" #include #include #include #include #include #include "compiler/Codec.h" #include "compiler/MachineConfig.h" #include "compiler/Stb.h" #include "compiler/TypeInfo.h" namespace compiler { namespace { /** * @brief 代码生成器 */ class Builder { public: /** * @brief 构造生成器 * @param proj 工程定义(cycle_limit / dt_ms / 哈希用) * @param units 全部源文件的 AST * @param link 链接结果(POU 顺序 / 符号) * @param cfg 机器定义(machine.toml 强校验后;指令 opcode / 类型 tag / FB 布局) * @param image 输出映像字节 * @param err 错误输出;可为 nullptr(静默) */ Builder(const Project& proj, const std::vector& units, const LinkResult& link, const MachineConfig& cfg, std::vector* image, std::string* err) : proj_(proj), units_(units), link_(link), cfg_(cfg), image_(image), err_(err) { // io 绑定分类(名已折小写;不创造变量,只影响操作码选择) for (const IoBinding& b : proj_.io) { std::string key = b.var; for (char& ch : key) { ch = static_cast(std::tolower(static_cast(ch))); } if (b.is_input) { io_input_[key] = true; } else { io_output_[key] = true; } } } /** * @brief 布局数据区 → 布局 FB 实例 → 逐 POU 建函数 → 拼映像 * @return true 成功;false(err 已写,前缀 "codegen error") */ bool run() { if (!layout_data()) { return false; } if (!layout_fb_instances()) { return false; } for (const LinkResult::PouScope& sc : link_.scopes) { const POU* pou = find_pou(sc.name); if (pou == nullptr) { continue; } FuncCtx f; f.name = sc.name; f.pou_name = sc.name; if (!build_function(*pou, &f)) { return false; } funcs_.push_back(std::move(f)); } return assemble_image(); } private: bool fail(const std::string& msg) { if (err_) { *err_ = "codegen error: " + msg; } return false; } /** * @brief 按名查配置 opcode(MachineConfig 强校验保证存在) * @param name 指令名(大写,如 "ADD") * @return opcode(找不到返回 0,不应发生) */ uint8_t opc(const char* name) const { const ConfigOp* op = cfg_.find_op(name); return op ? static_cast(op->opcode) : 0; } // ---- 指令发射(配置 opcode)---- Instr E_rr(const char* name, uint8_t rd, uint8_t rs) { return pack(opc(name), rd, rs, 0); } Instr E_rrr(const char* name, uint8_t rd, uint8_t ra, uint8_t rb) { return pack(opc(name), rd, ra, rb); } Instr E_imm(const char* name, uint8_t rd, uint16_t imm) { return pack(opc(name), rd, static_cast(imm & 0xFFu), static_cast((imm >> 8) & 0xFFu)); } Instr E_slot(const char* name, uint8_t rd, uint16_t slot) { return E_imm(name, rd, slot); } Instr E_jmp(int16_t off) { const uint16_t u = static_cast(off); return pack(opc("JMP"), 0, static_cast(u & 0xFFu), static_cast((u >> 8) & 0xFFu)); } Instr E_jc(const char* name, uint8_t r, int16_t off) { const uint16_t u = static_cast(off); return pack(opc(name), r, static_cast(u & 0xFFu), static_cast((u >> 8) & 0xFFu)); } Instr E_call(uint16_t fn_id) { return E_imm("CALL", 0, fn_id); } Instr E_ret() { return pack(opc("RET"), 0, 0, 0); } const POU* find_pou(const std::string& name) const { for (const SourceUnit& u : units_) { for (const POU& p : u.ast.pous) { if (p.name == name) { return &p; } } } return nullptr; } // 每函数的编译态 struct FuncCtx { std::string name; std::string pou_name; // 所属 POU 名(实例查找键) std::vector code; // 字节码 std::map regs; // 变量名 → 帧寄存器 uint8_t nlocals = 0; // 变量区终点 = 临时寄存器起始 uint8_t nregs = 0; // 峰值(变量 + 临时) uint8_t temp_used = 0; // 本语句已用临时数(语句结束清零) bool is_function = false; // FUNCTION(结果 r0 / 输入只读) std::string result_name; // FUNCTION 名(结果寄存器映射) }; // FB 实例:字段名 → 数据区槽号 struct InstFields { std::map field_addr; uint32_t base = 0; std::string type_name; // 实例的 FB 类型名(内建 / 用户) }; void begin_stmt(FuncCtx& f) { f.temp_used = 0; } uint8_t alloc_temp(FuncCtx& f) { const uint8_t r = f.nlocals + f.temp_used; ++f.temp_used; if (static_cast(f.nlocals) + f.temp_used > f.nregs) { f.nregs = f.nlocals + f.temp_used; } return r; } /** * @brief 编译一个 POU * @details 支持 PROGRAM / FUNCTION / FUNCTION_BLOCK: * FB 不生成业务字节码(调用点内联),只出空函数占位保持 fn_id 一致; * 帧约定(见 Doc/compiler/寄存器码.md):结果 r0、输入 r1..r7(只读)、 * 变量与临时全部从 r8 起 */ bool build_function(const POU& pou, FuncCtx* f) { if (pou.kind == PouKind::FunctionBlock) { f->nlocals = 8; f->nregs = 8; f->code.push_back(E_ret()); return true; } f->is_function = pou.kind == PouKind::Function; f->result_name = pou.name; if (f->is_function) { f->regs[pou.name] = 0; // 结果寄存器 r0 uint8_t idx = 1; for (const VarBlock& b : pou.blocks) { if (b.section != VarSection::Input) { continue; } for (const VarDecl& d : b.vars) { f->regs[d.name] = idx++; // 输入 r1..r7 } } } uint8_t r = 8; // 变量/临时基址(调用约定区 r0..r7 不占用) for (const VarBlock& b : pou.blocks) { if (b.section == VarSection::External || b.section == VarSection::Global) { continue; } if (f->is_function && b.section == VarSection::Input) { continue; } for (const VarDecl& d : b.vars) { if (d.type.kind == TypeKind::FbUser || d.type.kind == TypeKind::FbBuiltin) { continue; } f->regs[d.name] = r++; } } f->nlocals = r; f->nregs = r; for (const Stmt& st : pou.body) { begin_stmt(*f); if (!compile_stmt(*f, st)) { return false; } } f->code.push_back(E_ret()); return true; } bool compile_stmt(FuncCtx& f, const Stmt& st) { if (st.kind == StmtKind::If) { return compile_if(f, st); } if (st.kind == StmtKind::While) { return compile_while(f, st); } if (st.kind == StmtKind::FbCall) { return compile_fb_call(f, st); } if (st.kind != StmtKind::Assign) { return fail("statement not supported"); } // 内联 FB 体内:左值可能是实例字段(STORE_GLOBAL) if (inline_fields_) { const auto fit = inline_fields_->field_addr.find(st.target); if (fit != inline_fields_->field_addr.end()) { const uint8_t t = alloc_temp(f); if (!compile_expr(f, *st.value, t)) { return false; } f.code.push_back(E_slot("STORE_GLOBAL", t, fit->second)); return true; } } const auto it = f.regs.find(st.target); if (it == f.regs.end()) { return store_target(f, st.target, *st.value); } if (f.is_function && it->second >= 1 && it->second <= 7) { return fail("cannot write function input '" + st.target + "'"); } return compile_expr(f, *st.value, it->second); } bool compile_fb_call(FuncCtx& f, const Stmt& st) { const InstFields* inst = instance_of(f.pou_name, st.instance); if (inst == nullptr) { return fail("no instance '" + st.instance + "' in '" + f.pou_name + "'"); } for (const FbArg& a : st.args) { const auto it = inst->field_addr.find(a.name); if (it == inst->field_addr.end()) { return fail("unknown input '" + a.name + "' for '" + st.instance + "'"); } const uint8_t t = alloc_temp(f); if (!compile_expr(f, *a.value, t)) { return false; } f.code.push_back(E_slot("STORE_GLOBAL", t, it->second)); } // 内建 FB:配置里必须有对应 op 行(强校验保证)→ 直接查配置 opcode const std::string& tn = inst->type_name; if (cfg_.find_op("CAL_" + uppercase_of(tn)) != nullptr && (tn == "ton" || tn == "tof" || tn == "tp" || tn == "ctu" || tn == "ctd" || tn == "ctud" || tn == "r_trig" || tn == "f_trig")) { const std::string opname = "CAL_" + uppercase_of(tn); f.code.push_back(E_slot(opname.c_str(), 0, inst->base)); return true; } const POU* fb = find_pou(inst->type_name); if (fb == nullptr) { return fail("no FB type '" + inst->type_name + "'"); } return compile_fb_inline(f, *fb, *inst); } static std::string uppercase_of(const std::string& s) { std::string out = s; for (char& ch : out) { ch = static_cast(std::toupper(static_cast(ch))); } return out; } bool compile_fb_inline(FuncCtx& f, const POU& fb, const InstFields& inst) { const InstFields* saved = inline_fields_; inline_fields_ = &inst; for (const Stmt& s : fb.body) { begin_stmt(f); if (!compile_stmt(f, s)) { return false; } } inline_fields_ = saved; return true; } bool compile_while(FuncCtx& f, const Stmt& st) { const size_t loop = f.code.size(); const uint8_t t = alloc_temp(f); if (!compile_expr(f, *st.cond, t)) { return false; } const size_t jf_idx = f.code.size(); f.code.push_back(E_jc("JF", t, 0)); for (const Stmt& s : st.body) { begin_stmt(f); if (!compile_stmt(f, s)) { return false; } } const size_t jmp_idx = f.code.size(); f.code.push_back(E_jmp(0)); patch_jump(f, jmp_idx, loop); patch_jump(f, jf_idx, f.code.size()); return true; } bool compile_if(FuncCtx& f, const Stmt& st) { std::vector*>> branches; branches.push_back({st.cond.get(), &st.body}); for (const IfBranch& b : st.elsifs) { branches.push_back({b.cond.get(), &b.body}); } const bool has_else = !st.else_body.empty(); std::vector end_jmps; for (size_t i = 0; i < branches.size(); ++i) { const uint8_t t = alloc_temp(f); if (!compile_expr(f, *branches[i].first, t)) { return false; } const size_t jf_idx = f.code.size(); f.code.push_back(E_jc("JF", t, 0)); for (const Stmt& s : *branches[i].second) { begin_stmt(f); if (!compile_stmt(f, s)) { return false; } } if (i + 1 < branches.size() || has_else) { const size_t jmp_idx = f.code.size(); f.code.push_back(E_jmp(0)); end_jmps.push_back(jmp_idx); } patch_jump(f, jf_idx, f.code.size()); } if (has_else) { for (const Stmt& s : st.else_body) { begin_stmt(f); if (!compile_stmt(f, s)) { return false; } } } const size_t end = f.code.size(); for (const size_t j : end_jmps) { patch_jump(f, j, end); } return true; } bool store_target(FuncCtx& f, const std::string& target, const Expr& value) { const auto git = link_.global_index.find(target); if (git == link_.global_index.end()) { return fail("no storage for '" + target + "'"); } if (io_input_.count(target)) { return fail("cannot write to input '" + target + "'"); } const uint8_t tmp = alloc_temp(f); if (!compile_expr(f, value, tmp)) { return false; } f.code.push_back(E_slot(store_name(target), tmp, static_cast(git->second))); return true; } bool compile_expr(FuncCtx& f, const Expr& e, uint8_t rd) { if (e.kind == ExprKind::LitBool || e.kind == ExprKind::LitInt || e.kind == ExprKind::LitTime) { const char* type_name = e.kind == ExprKind::LitBool ? "BOOL" : e.kind == ExprKind::LitInt ? "INT" : "TIME"; f.code.push_back(E_imm("LOADK", rd, const_id(type_name, e.int_value))); return true; } if (e.kind == ExprKind::VarRef) { if (inline_fields_) { const auto fit = inline_fields_->field_addr.find(e.name); if (fit != inline_fields_->field_addr.end()) { f.code.push_back(E_slot("LOAD_GLOBAL", rd, fit->second)); return true; } } const auto sit = f.regs.find(e.name); if (sit != f.regs.end()) { f.code.push_back(E_rr("MOVE", rd, sit->second)); return true; } const auto git = link_.global_index.find(e.name); if (git != link_.global_index.end()) { f.code.push_back( E_slot(load_name(e.name), rd, static_cast(git->second))); return true; } return fail("no register or slot for '" + e.name + "'"); } if (e.kind == ExprKind::Field) { const InstFields* inst = instance_of(f.pou_name, e.name); if (inst == nullptr) { return fail("no instance '" + e.name + "' in '" + f.pou_name + "'"); } const auto fit = inst->field_addr.find(e.field); if (fit == inst->field_addr.end()) { return fail("unknown field '" + e.field + "' for '" + e.name + "'"); } f.code.push_back(E_slot("LOAD_GLOBAL", rd, fit->second)); return true; } if (e.kind == ExprKind::Add || e.kind == ExprKind::Sub || e.kind == ExprKind::Mul || e.kind == ExprKind::Div) { const uint8_t l = alloc_temp(f); if (!compile_expr(f, *e.lhs, l)) { return false; } const uint8_t r = alloc_temp(f); if (!compile_expr(f, *e.rhs, r)) { return false; } f.code.push_back(E_rrr(arith_name(e.kind), rd, l, r)); return true; } if (e.kind == ExprKind::Cmp) { const uint8_t l = alloc_temp(f); if (!compile_expr(f, *e.lhs, l)) { return false; } const uint8_t r = alloc_temp(f); if (!compile_expr(f, *e.rhs, r)) { return false; } f.code.push_back(E_rrr(cmp_name(e.op), rd, l, r)); return true; } if (e.kind == ExprKind::Not) { const uint8_t t = alloc_temp(f); if (!compile_expr(f, *e.operand, t)) { return false; } f.code.push_back(E_rr("NOT", rd, t)); return true; } if (e.kind == ExprKind::And || e.kind == ExprKind::Or) { if (!compile_expr(f, *e.lhs, rd)) { return false; } const char* jname = (e.kind == ExprKind::And) ? "JF" : "JT"; const size_t jmp_idx = f.code.size(); f.code.push_back(E_jc(jname, rd, 0)); const uint8_t t = alloc_temp(f); if (!compile_expr(f, *e.rhs, t)) { return false; } f.code.push_back(E_rr("MOVE", rd, t)); patch_jump(f, jmp_idx, f.code.size()); return true; } if (e.kind == ExprKind::Call) { if (e.args.size() > 7) { return fail("too many arguments (max 7)"); } std::vector args; for (const auto& a : e.args) { const uint8_t t = alloc_temp(f); if (!compile_expr(f, *a, t)) { return false; } args.push_back(t); } const int fn_id = find_fn_id(e.name); if (fn_id < 0) { return fail("no fn_id for '" + e.name + "'"); } for (size_t i = 0; i < args.size(); ++i) { f.code.push_back(E_rr("MOVE", static_cast(1 + i), args[i])); } f.code.push_back(E_call(static_cast(fn_id))); f.code.push_back(E_rr("MOVE", rd, 0)); return true; } return fail("expression not supported"); } int find_fn_id(const std::string& name) const { for (size_t i = 0; i < link_.scopes.size(); ++i) { if (link_.scopes[i].name == name) { return static_cast(i); } } return -1; } void patch_jump(FuncCtx& f, size_t idx, size_t target_idx) { const int16_t off = static_cast( static_cast(target_idx) - (static_cast(idx) + 1)); const Instr w = f.code[idx]; const uint16_t u = static_cast(off); f.code[idx] = pack(op_of(w), rd_of(w), static_cast(u & 0xFFu), static_cast((u >> 8) & 0xFFu)); } // ---- 操作码名(MachineConfig 已强校验存在)---- static const char* arith_name(ExprKind k) { switch (k) { case ExprKind::Add: return "ADD"; case ExprKind::Sub: return "SUB"; case ExprKind::Mul: return "MUL"; case ExprKind::Div: return "DIV"; default: return "ADD"; } } static const char* cmp_name(BinOp op) { switch (op) { case BinOp::Eq: return "CMP_EQ"; case BinOp::Ne: return "CMP_NE"; case BinOp::Lt: return "CMP_LT"; case BinOp::Le: return "CMP_LE"; case BinOp::Gt: return "CMP_GT"; case BinOp::Ge: return "CMP_GE"; default: return "CMP_EQ"; } } const char* load_name(const std::string& name) const { return io_input_.count(name) ? "LOAD_I" : "LOAD_GLOBAL"; } const char* store_name(const std::string& name) const { return io_output_.count(name) ? "STORE_Q" : "STORE_GLOBAL"; } bool layout_data() { for (const Symbol& s : link_.globals) { if (s.address > 0xFFFF) { return fail("data area exceeds slot range"); } bool has_init = false; int64_t init = 0; init_of(s.name, &has_init, &init); const int64_t v = has_init ? init : 0; const size_t off = static_cast(s.address) * 8; data_.resize(off + 8, 0); // 初值按类型元数据写(配置别名 → 基元宽度/浮点) const TypeMeta tm = type_meta(cfg_, s.type_name); if (!tm) { return fail("no type in machine.toml for '" + s.type_name + "'"); } if (tm.is_float()) { return fail("float initializer not supported yet"); } if (tm.width() == 2) { const uint16_t iv = static_cast(v); data_[off] = static_cast(iv & 0xFFu); data_[off + 1] = static_cast((iv >> 8) & 0xFFu); } else if (tm.width() == 8) { const uint64_t tv = static_cast(v); for (int i = 0; i < 8; ++i) { data_[off + i] = static_cast((tv >> (8 * i)) & 0xFFu); } } else { data_[off] = v ? 1 : 0; } } return true; } bool layout_fb_instances() { uint32_t cur = static_cast(data_.size() / 8); bool any = false; for (const LinkResult::PouScope& sc : link_.scopes) { for (const Symbol& s : sc.syms) { if (s.kind != SymbolKind::FbInstance) { continue; } const auto lay = sc.fb_instances.find(s.name); if (lay == sc.fb_instances.end()) { return fail("no layout for instance '" + s.name + "'"); } InstFields inst; inst.base = cur; inst.type_name = s.type_name; for (size_t i = 0; i < lay->second.fields.size(); ++i) { inst.field_addr[lay->second.fields[i].name] = cur + i; } cur += static_cast(lay->second.fields.size()); instances_[sc.name + "/" + s.name] = inst; any = true; } } if (any) { data_.resize(static_cast(cur) * 8, 0); } return true; } const InstFields* instance_of(const std::string& pou, const std::string& name) const { const auto it = instances_.find(pou + "/" + name); return it == instances_.end() ? nullptr : &it->second; } void init_of(const std::string& name, bool* has_init, int64_t* init) const { for (const SourceUnit& u : units_) { for (const VarBlock& b : u.ast.globals) { for (const VarDecl& d : b.vars) { if (d.name == name) { *has_init = d.has_init; *init = d.init_value; return; } } } } *has_init = false; *init = 0; } /** * @brief 取常量表 id(无则追加) * @param type_name 语言类型名("BOOL"/"INT"/"TIME")→ 配置 tag(契约) * @param value 常量值 * @return const_id(u16) */ uint16_t const_id(const char* type_name, int64_t value) { const TypeMeta tm = type_meta(cfg_, type_name); const uint32_t tag = tm ? tm.tag() : 0; for (size_t i = 0; i < consts_.size(); ++i) { if (consts_[i].tag == tag && consts_[i].value == static_cast(value)) { return static_cast(i); } } consts_.push_back({tag, static_cast(value)}); return static_cast(consts_.size() - 1); } bool assemble_image() { const uint32_t off_const = static_cast(kHeaderSize); const uint32_t off_funcs = off_const + static_cast(consts_.size()) * static_cast(kConstEntrySize); uint32_t off_code = off_funcs + static_cast(funcs_.size()) * static_cast(kFuncRowSize); uint32_t code_total = 0; for (const FuncCtx& f : funcs_) { code_total += static_cast(f.code.size()) * 4; } const uint32_t off_data = off_code + code_total; const uint32_t off_end = off_data + static_cast(data_.size()); std::vector& b = *image_; b.assign(off_end, 0); put_le32(b, 0, kMagic); put_le32(b, 4, kVersion); put_le32(b, 8, proj_.cycle_limit); put_le32(b, 12, proj_.dt_ms); uint64_t hash = kFnvBasis; if (!compute_project_hash(proj_, &hash, err_)) { return false; } put_le64(b, 16, hash); uint32_t entry = 0; for (size_t i = 0; i < funcs_.size(); ++i) { if (funcs_[i].name == "main") { entry = static_cast(i); } } put_le32(b, 24, entry); put_le32(b, 28, static_cast(link_.globals.size())); put_le32(b, 32, 0); put_le32(b, 36, 0); put_le32(b, 40, 0); put_le32(b, 44, static_cast(consts_.size())); put_le32(b, 48, static_cast(funcs_.size())); put_le32(b, 52, off_const); put_le32(b, 56, off_funcs); put_le32(b, 60, off_code); put_le32(b, 64, off_data); put_le32(b, 68, off_data); for (size_t i = 0; i < consts_.size(); ++i) { const size_t o = off_const + i * kConstEntrySize; put_le32(b, o, consts_[i].tag); put_le64(b, o + 4, consts_[i].value); } uint32_t c = off_code; for (size_t i = 0; i < funcs_.size(); ++i) { const FuncCtx& f = funcs_[i]; const size_t o = off_funcs + i * kFuncRowSize; put_le32(b, o, f.nregs); put_le32(b, o + 8, static_cast(f.code.size())); for (const Instr in : f.code) { put_le32(b, c, in); c += 4; } } uint32_t acc = 0; for (size_t i = 0; i < funcs_.size(); ++i) { const size_t o = off_funcs + i * kFuncRowSize; put_le32(b, o + 4, acc); acc += static_cast(funcs_[i].code.size()) * 4; } for (size_t i = 0; i < data_.size(); ++i) { b[off_data + i] = data_[i]; } return true; } static void put_le32(std::vector& b, size_t off, uint32_t v) { b[off + 0] = static_cast(v & 0xFFu); b[off + 1] = static_cast((v >> 8) & 0xFFu); b[off + 2] = static_cast((v >> 16) & 0xFFu); b[off + 3] = static_cast((v >> 24) & 0xFFu); } static void put_le64(std::vector& b, size_t off, uint64_t v) { for (int i = 0; i < 8; ++i) { b[off + i] = static_cast((v >> (8 * i)) & 0xFFu); } } // ---- 成员 ---- const Project& proj_; const std::vector& units_; const LinkResult& link_; const MachineConfig& cfg_; std::vector* image_; std::string* err_; std::vector funcs_; std::vector consts_; std::map io_input_; std::map io_output_; std::vector data_; std::map instances_; const InstFields* inline_fields_ = nullptr; }; } // namespace bool codegen_project(const Project& proj, const std::vector& units, const LinkResult& link, const MachineConfig& cfg, std::vector* image, std::string* err) { Builder b(proj, units, link, cfg, image, err); return b.run(); } } // namespace compiler