/** * @file linker_test.cpp * @brief 链接测试:用例 09/10/11/12/16/19 + line1 正例 * @author * @date 2026-08-21 */ #include #include #include #include "compiler/Linker.h" #include "compiler/Project.h" #ifndef REPO_ROOT #define REPO_ROOT "." #endif static int g_checks = 0; #define CHECK(cond) \ do { \ if (!(cond)) { \ std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ return false; \ } \ ++g_checks; \ } while (0) // 从用例目录加载工程并链接 static bool link_case(const char* dir, compiler::LinkResult* out, std::string* err) { using namespace compiler; const std::string toml = std::string(REPO_ROOT) + "/tests/cases/" + dir + "/project.toml"; Project p; if (!parse_project(toml, &p, err)) { return false; } std::vector units; for (const std::string& f : compile_files(p)) { SourceUnit u; if (!load_unit(p.base_dir + "/" + f, &u, err)) { return false; } units.push_back(std::move(u)); } return link_project(p, units, out, err); } static bool expect_link_err(const char* dir, const char* keyword) { compiler::LinkResult r; std::string err; if (link_case(dir, &r, &err)) { std::printf("FAIL %s: linked ok\n", dir); return false; } if (err.find("link error") != 0) { std::printf("FAIL %s: want 'link error', got '%s'\n", dir, err.c_str()); return false; } if (err.find(keyword) == std::string::npos) { std::printf("FAIL %s: want '%s', got '%s'\n", dir, keyword, err.c_str()); return false; } ++g_checks; return true; } // ---- 1. 用例 09:GVL + VAR_EXTERNAL 同一槽 ---- static bool test_case09() { using namespace compiler; LinkResult r; std::string err; CHECK(link_case("09_gvl_external", &r, &err)); CHECK(r.globals.size() == 1); CHECK(r.globals[0].name == "g1"); CHECK(r.globals[0].kind == SymbolKind::Global); CHECK(r.globals[0].type_name == "int"); CHECK(r.globals[0].address == 0); CHECK(r.scopes.size() == 1); const LinkResult::PouScope& main = r.scopes[0]; CHECK(main.name == "main"); bool saw_external = false; for (const Symbol& s : main.syms) { if (s.name == "g1") { saw_external = true; CHECK(s.kind == SymbolKind::External); CHECK(s.address == 0); // 同一槽 CHECK(s.type_name == "int"); } } // 符号表打印:name kind type address file const std::string dump = dump_symbols(r); CHECK(dump.find("g1 global int 0 ") != std::string::npos); CHECK(dump.find("[main]") != std::string::npos); CHECK(dump.find("g1 external int 0 ") != std::string::npos); return true; } // ---- 2. 用例 10 / 11 / 12 ---- static bool test_neg_cases() { if (!expect_link_err("10_gvl_wrong_file", "VAR_GLOBAL only allowed in gvl file")) { return false; } if (!expect_link_err("11_duplicate_global", "duplicate global")) { return false; } if (!expect_link_err("12_io_unknown_var", "io.var 'Nope' not in GVL")) { return false; } return true; } // ---- 3. 用例 16:未声明实例 ---- static bool test_case16() { if (!expect_link_err("16_fb_undeclared", "undeclared FB instance")) { return false; } return true; } // ---- 4. 用例 19:循环调用 ---- static bool test_case19() { if (!expect_link_err("19_recursive_call", "recursive call")) { return false; } return true; } // ---- 5. line1 正例:全局槽序 / FB 布局 / 字段 / 实例 ---- static bool test_line1() { using namespace compiler; LinkResult r; std::string err; const std::string toml = std::string(REPO_ROOT) + "/examples/line1/project.toml"; Project p; CHECK(parse_project(toml, &p, &err)); std::vector units; for (const std::string& f : compile_files(p)) { SourceUnit u; CHECK(load_unit(p.base_dir + "/" + f, &u, &err)); units.push_back(std::move(u)); } CHECK(link_project(p, units, &r, &err)); // 全局槽序 = 声明顺序 CHECK(r.globals.size() == 4); CHECK(r.globals[0].name == "emergencystop"); CHECK(r.globals[1].name == "i0_0"); CHECK(r.globals[2].name == "i0_1"); CHECK(r.globals[3].name == "q0_0"); // 用户 FB 布局:MotorStarter 字段 = input/output(external 不是字段) const auto it = r.fb_types.find("motorstarter"); CHECK(it != r.fb_types.end()); CHECK(it->second.fields.size() == 3); CHECK(it->second.fields[0].name == "start"); CHECK(it->second.fields[1].name == "stop"); CHECK(it->second.fields[2].name == "q"); CHECK(it->second.fields[2].type == TypeKind::Bool); // 内建 TON 布局(用例 17 的 t) const FbLayout* ton = nullptr; for (const auto& [name, lay] : r.fb_types) { (void)name; (void)lay; } // 直接构造 TON 实例验证内建表(见 case17 用) // main 作用域:starter 实例 + 字段引用 CHECK(r.scopes.size() == 2); // motorstarter + main const LinkResult::PouScope& main = r.scopes[1]; bool saw_starter = false; for (const Symbol& s : main.syms) { if (s.name == "starter") { saw_starter = true; CHECK(s.kind == SymbolKind::FbInstance); CHECK(s.type_name == "motorstarter"); } } CHECK(saw_starter); const auto sit = main.fb_instances.find("starter"); CHECK(sit != main.fb_instances.end()); CHECK(sit->second.fields.size() == 3); return true; } // ---- 6. 内建 FB 布局(TON)---- static bool test_builtin() { using namespace compiler; LinkResult r; std::string err; const std::string toml = std::string(REPO_ROOT) + "/tests/cases/17_ton/project.toml"; Project p; CHECK(parse_project(toml, &p, &err)); std::vector units; for (const std::string& f : compile_files(p)) { SourceUnit u; CHECK(load_unit(p.base_dir + "/" + f, &u, &err)); units.push_back(std::move(u)); } CHECK(link_project(p, units, &r, &err)); CHECK(r.scopes.size() == 1); const LinkResult::PouScope& main = r.scopes[0]; const auto it = main.fb_instances.find("t"); CHECK(it != main.fb_instances.end()); CHECK(it->second.fields.size() == 4); CHECK(it->second.fields[0].name == "in" && it->second.fields[0].type == TypeKind::Bool); CHECK(it->second.fields[1].name == "pt" && it->second.fields[1].type == TypeKind::Time); CHECK(it->second.fields[2].name == "q" && it->second.fields[2].type == TypeKind::Bool); CHECK(it->second.fields[3].name == "et" && it->second.fields[3].type == TypeKind::Time); return true; } int main() { if (!test_case09()) return 1; if (!test_neg_cases()) return 1; if (!test_case16()) return 1; if (!test_case19()) return 1; if (!test_line1()) return 1; if (!test_builtin()) return 1; std::printf("linker_test: %d checks passed\n", g_checks); return 0; }