实现 12.6 符号表与链接:全局定槽、引用解析、循环检测。

- Linker.h/cpp:收集导出(仅 gvl 文件顶层 VAR_GLOBAL、声明顺序定槽、重复名拒绝)
- VAR_EXTERNAL 接同一槽、类型一致;FB 实例必须先声明;字段存在性校验;函数调用图 DFS 环检测
- io.var 必须在 GVL;内建 TON/TOF/CTU 布局冻结;用户 FB 字段 = input/output/var(external 不是字段)
- 函数名赋值 = 结果值写入(用例 13 约定);dump_symbols 打印 name kind type address file
- linker_test:51 断言(用例 09/10/11/12/16/19 + line1/17 正例),ctest 7/7;20 用例扫描 15 绿 5 按期望拒
This commit is contained in:
2026-08-21 11:14:03 +08:00
parent 93d2c96f20
commit 6f02c18bbb
7 changed files with 985 additions and 1 deletions
+11
View File
@@ -45,3 +45,14 @@ target_compile_definitions(parser_test PRIVATE
add_test(NAME parser_syntax
COMMAND parser_test)
# 链接测试:用例 09/10/11/12/16/19 + line1 正例(REPO_ROOT 注入源目录绝对路径)
add_executable(linker_test
./src/linker_test.cpp)
target_link_libraries(linker_test PRIVATE compiler)
target_compile_definitions(linker_test PRIVATE
REPO_ROOT="${CMAKE_SOURCE_DIR}")
add_test(NAME linker_links
COMMAND linker_test)
+229
View File
@@ -0,0 +1,229 @@
/**
* @file linker_test.cpp
* @brief 链接测试:用例 09/10/11/12/16/19 + line1 正例
* @author
* @date 2026-08-21
*/
#include <cstdio>
#include <string>
#include <vector>
#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<SourceUnit> 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. 用例 09GVL + 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<SourceUnit> 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/outputexternal 不是字段)
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<SourceUnit> 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;
}