实现 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
+111
View File
@@ -0,0 +1,111 @@
# 符号表与链接(12.6
compiler 模块的符号收集与链接。输入:`Project`toml+ 各 `.st` 文件的 `Unit`AST,见 [`语法.md`](语法.md));输出:链接结果(全局槽表、POU 符号表、函数表、FB 实例布局)。全工程定案见 [`初步计划.md`](../初步计划.md) 12.6。
## 做 / 不做
**做**
- 先扫完全部文件再解析引用(禁止边解析边执行)
- 全局收集:仅 `gvl.file` 顶层 `VAR_GLOBAL`,声明顺序 = 全局槽顺序
- `VAR_EXTERNAL` 接已有槽,种类/类型一致,不是新变量
- `FUNCTION` 标无状态;`FUNCTION_BLOCK` 必须先有实例才能调用
- `CALL` 目标解析成 `fn_id`(立即数),无函数指针
- 校验:同名全局、未声明 `io.var`、重复 POU 名、未声明 FB 实例、函数循环调用 → 失败
- FB 实例布局:按类型算字段清单(输入/输出/内部/内建 TON/TOF/CTU 固定字段)
**不做(第一版)**
- 寄存器分配(12.8)、类型检查(12.7)
- 多 GVL 文件、I/Q/M 与全局分槽(v1 **单一数据区**,见下)
## 数据区定址(v1 定案)
- **单一数据区**:全部全局(含 I/Q/M)按 `globals.st` 声明顺序编槽号 0..n-1
- 映像头 `n_globals` = 声明总数;`n_i` / `n_q` / `n_m` = 0(第一版不拆,见 [`初步计划.md`](../初步计划.md) 5.1
- `LOAD_I` / `STORE_Q` / `LOAD_GLOBAL` / `STORE_GLOBAL` 操作码不同,槽号指向同一数据区
- io 绑定(toml)只负责 channel/bit → 槽号映射(sidecar),不创造变量
## 符号表一行
```cpp
enum class SymbolKind { Global, External, Local, Input, Output, Pou, FbInstance, Const };
struct Symbol {
SymbolKind kind;
std::string name; // 小写
std::string type_name; // bool / int / time / fb 类型名(小写)
uint32_t address; // 全局槽号(Global/External);POU 局部暂 012.8 分配)
std::string source_file;
uint32_t line, col;
};
```
`kind` 与 12.1 冻结表一致:`global` / `external` / `local` / `input` / `output` / `pou` / `fb_instance` / `const``const` 本期未用,保留)。
## 链接流程
```text
1. 收集导出
遍历全部文件:
- 顶层 globals:仅 gvl.file 允许;同名 → duplicate global;声明顺序编槽号
- POU 导出:program / function / fb 名登记;重复 → duplicate POU
2. 逐 POU 建局部符号
VAR / VAR_INPUT / VAR_OUTPUT → local / input / output
VAR_EXTERNAL → 查全局表,必须存在、类型一致 → External(同一槽)
POU 内 VAR_GLOBAL → error(仅 gvl 顶层允许)
3. 语句引用解析
- FB 调用 instance(...) → 局部表查实例,类型必须是 FB → 否则 undeclared FB instance
- 字段读 fb.field → 实例类型有该字段(用户 FB 的 input/output;内建固定字段)
- VarRef / 赋值左值 → 局部表或全局表查得到 → 否则 undeclared identifier
- 函数调用 fn(...) → POU 表查得到且是 FUNCTION → 记依赖边;FUNCTION 不得调用 PROGRAM/FB
4. 循环调用检测
函数依赖图 DFS 找环 → recursive call(用例 19
5. io 校验
io.var 必须在全局表 → 否则 io.var not in GVL(用例 12
6. 输出
全局表 / POU 符号表 / 函数表(fn_id 按收集序)/ FB 实例布局
```
## FB 实例布局
- 用户 FB:字段 = 全部 `VAR_INPUT` + `VAR_OUTPUT` + `VAR`(内部),按声明段序排
- 内建(冻结,isa 规格):
| FB | 字段(名 → 类型) |
|---|---|
| TON | in BOOL, pt TIME, q BOOL, et TIME |
| TOF | in BOOL, pt TIME, q BOOL, et TIME |
| CTU | cu BOOL, r BOOL, pv INT, q BOOL, cv INT |
- 实例布局 = 类型字段表的一份拷贝;字段偏移 12.8 按对齐规则算
## 错误
稳定前缀 `link error`
```text
link error: duplicate global 'g1' (globals.st, line 4, col 1)
link error: VAR_GLOBAL only allowed in gvl file (other.st, line 2, col 1)
link error: io.var 'Nope' not in GVL (project.toml)
link error: undeclared FB instance 'starter' (main.st, line 4, col 5)
link error: recursive call involving 'a' (main.st, line 2, col 4)
link error: undeclared identifier 'x' (main.st, line 3, col 3)
```
## 完成标准
1. 用例 9(同一槽)、10(非 GVL 写全局)、11(同名全局)、12(io.var 未声明)、16(未声明实例)、19(循环调用)按表变绿
2. 符号表能打印 `name kind type address file`
3. 全部构建 + `ctest` 无回归
---
## 执行计划(单步确认)
1. **`Linker.h`(新建 `compiler/include/compiler/Linker.h`**`Symbol`/`SymbolKind` + 链接结果结构 + `link_project` 声明
2. **`Linker.cpp` 前半**:收集导出(gvl 校验 / 全局定槽 / POU 登记)+ 逐 POU 局部符号
3. **`Linker.cpp` 后半**:引用解析(FB 实例 / 字段 / 变量 / 函数)+ 循环检测 + io 校验
4. **`linker_test``tests/src/linker_test.cpp` + CMake**:用例 09/10/11/12/16/19 + 符号表打印断言
5. **验证**`cmake --build` + `ctest`(新增 `linker_links` 后 7/7 全绿)
6. **提交**`Linker.h/cpp` + `linker_test` + 文档
+2
View File
@@ -10,6 +10,7 @@ doc/
compiler/编译管线.md
compiler/词法.md
compiler/语法.md
compiler/符号表与链接.md
vm/扫描周期.md
executor/执行器入口.md
```
@@ -21,6 +22,7 @@ doc/
| [`compiler/编译管线.md`](compiler/编译管线.md) | 编译器边界与管线 |
| [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 |
| [`compiler/语法.md`](compiler/语法.md) | 12.5 语法:AST 结构、文法、拒绝清单 |
| [`compiler/符号表与链接.md`](compiler/符号表与链接.md) | 12.6 符号表与链接:数据区定址、FB 布局、错误类别 |
| [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 |
| [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 |
+2 -1
View File
@@ -10,7 +10,8 @@ project(Compiler
add_library(compiler STATIC
./src/Lexer.cpp
./src/Project.cpp
./src/Parser.cpp)
./src/Parser.cpp
./src/Linker.cpp)
target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include)
+82
View File
@@ -0,0 +1,82 @@
/**
* @file Linker.h
* @brief 符号表与链接
* @author
* @date 2026-08-21
*/
#pragma once
#include <cstdint>
#include <map>
#include <string>
#include <vector>
#include "compiler/Parser.h"
#include "compiler/Project.h"
namespace compiler {
// 符号种类(与 Doc/初步计划.md 12.1 一致)
enum class SymbolKind { Global, External, Local, Input, Output, Pou, FbInstance, Const };
struct Symbol {
SymbolKind kind = SymbolKind::Local;
std::string name; // 小写
std::string type_name; // bool / int / time / fb 类型名(小写)
uint32_t address = 0; // 全局槽号(Global/External);局部暂 012.8 分配)
std::string source_file;
uint32_t line = 0;
uint32_t col = 0;
};
// FB 类型字段(实例布局用;v1 字段类型仅 BOOL/INT/TIME
struct FbField {
std::string name;
TypeKind type = TypeKind::Bool;
};
struct FbLayout {
std::string type_name; // 类型名(小写)
std::vector<FbField> fields; // 段序:input → output → 内部 var
};
// 一个源文件的 AST(供链接输入)
struct SourceUnit {
std::string path;
Unit ast;
};
// 链接结果
struct LinkResult {
// 全局槽表:下标即槽号(声明顺序)
std::vector<Symbol> globals;
std::map<std::string, uint32_t> global_index; // 名 → 槽号
// 用户 FB 类型布局(按类型名)
std::map<std::string, FbLayout> fb_types;
// 每 POU 的局部符号与 FB 实例
struct PouScope {
std::string name;
PouKind kind = PouKind::Program;
std::vector<Symbol> syms; // local/input/output/external/fb_instance
std::map<std::string, FbLayout> fb_instances; // 实例名 → 布局
};
std::vector<PouScope> scopes;
// 函数表顺序(PROGRAM / FUNCTION / FUNCTION_BLOCK 名,收集序)
std::vector<std::string> fn_order;
};
// 链接一个工程:校验 GVL、定全局槽、解析 VAR_EXTERNAL、FB 实例与字段、
// 函数循环调用检测、io 校验。失败返回 falseerr 前缀 "link error"。
bool link_project(const Project& proj, const std::vector<SourceUnit>& units,
LinkResult* out, std::string* err);
// 读取并解析一个 .st 文件成 SourceUnit
bool load_unit(const std::string& path, SourceUnit* out, std::string* err);
// 符号表文本:每行 "name kind type address file"(全局表 + 各 POU 局部表)
std::string dump_symbols(const LinkResult& r);
}
+548
View File
@@ -0,0 +1,548 @@
/**
* @file Linker.cpp
* @brief 符号表与链接
* @author
* @date 2026-08-21
*/
#include "compiler/Linker.h"
#include <cctype>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace compiler {
namespace {
const char* kind_name(SymbolKind k) {
switch (k) {
case SymbolKind::Global: return "global";
case SymbolKind::External: return "external";
case SymbolKind::Local: return "local";
case SymbolKind::Input: return "input";
case SymbolKind::Output: return "output";
case SymbolKind::Pou: return "pou";
case SymbolKind::FbInstance: return "fb_instance";
case SymbolKind::Const: return "const";
}
return "?";
}
std::string type_str(const TypeRef& t) {
switch (t.kind) {
case TypeKind::Bool: return "bool";
case TypeKind::Int: return "int";
case TypeKind::Time: return "time";
case TypeKind::FbBuiltin:
case TypeKind::FbUser: return t.name;
}
return "?";
}
bool is_scalar(TypeKind k) {
return k == TypeKind::Bool || k == TypeKind::Int || k == TypeKind::Time;
}
// 内建 FB 布局(冻结,见 Doc/compiler/符号表与链接.md
const FbLayout kTonLayout{"ton", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time},
{"q", TypeKind::Bool}, {"et", TypeKind::Time}}};
const FbLayout kTofLayout{"tof", {{"in", TypeKind::Bool}, {"pt", TypeKind::Time},
{"q", TypeKind::Bool}, {"et", TypeKind::Time}}};
const FbLayout kCtuLayout{"ctu", {{"cu", TypeKind::Bool}, {"r", TypeKind::Bool},
{"pv", TypeKind::Int}, {"q", TypeKind::Bool},
{"cv", TypeKind::Int}}};
const FbLayout* builtin_layout(const std::string& name) {
if (name == "ton") return &kTonLayout;
if (name == "tof") return &kTofLayout;
if (name == "ctu") return &kCtuLayout;
return nullptr;
}
class Linker {
public:
Linker(const Project& proj, const std::vector<SourceUnit>& units,
LinkResult* out, std::string* err)
: proj_(proj), units_(units), out_(out), err_(err) {
gvl_path_ = std::filesystem::weakly_canonical(
std::filesystem::path(proj.base_dir) / proj.gvl_file);
}
bool run() {
if (!collect_exports()) return false;
if (!collect_fb_types()) return false;
if (!resolve_scopes()) return false;
if (!check_cycles()) return false;
if (!check_io()) return false;
if (!has_program_main()) {
return fail("", "missing entry 'program MAIN'", 0, 0);
}
return true;
}
private:
bool fail(const std::string& file, const std::string& msg,
uint32_t line, uint32_t col) {
if (err_) {
char buf[256];
if (line) {
std::snprintf(buf, sizeof buf, " (%s, line %u, col %u)",
file.c_str(), line, col);
} else {
std::snprintf(buf, sizeof buf, " (%s)", file.c_str());
}
*err_ = "link error: " + msg + buf;
}
return false;
}
bool is_gvl_file(const std::string& path) const {
if (proj_.gvl_file.empty()) {
return false;
}
return std::filesystem::weakly_canonical(path) == gvl_path_;
}
// ---- 1. 收集导出 ----
bool collect_exports() {
for (const SourceUnit& u : units_) {
const bool is_gvl = is_gvl_file(u.path);
for (const VarBlock& b : u.ast.globals) {
if (!is_gvl) {
return fail(u.path, "VAR_GLOBAL only allowed in gvl file",
b.vars.empty() ? 0 : b.vars[0].line,
b.vars.empty() ? 0 : b.vars[0].col);
}
for (const VarDecl& d : b.vars) {
if (out_->global_index.count(d.name)) {
return fail(u.path, "duplicate global '" + d.name + "'",
d.line, d.col);
}
Symbol s;
s.kind = SymbolKind::Global;
s.name = d.name;
s.type_name = type_str(d.type);
s.address = static_cast<uint32_t>(out_->globals.size());
s.source_file = u.path;
s.line = d.line;
s.col = d.col;
out_->global_index[s.name] = s.address;
out_->globals.push_back(s);
}
}
for (const POU& p : u.ast.pous) {
for (const LinkResult::PouScope& sc : out_->scopes) {
if (sc.name == p.name) {
return fail(u.path, "duplicate POU '" + p.name + "'",
0, 0);
}
}
LinkResult::PouScope sc;
sc.name = p.name;
sc.kind = p.kind;
out_->scopes.push_back(std::move(sc));
out_->fn_order.push_back(p.name);
}
}
return true;
}
// ---- 2. 用户 FB 类型布局 ----
bool collect_fb_types() {
for (const SourceUnit& u : units_) {
for (const POU& p : u.ast.pous) {
if (p.kind != PouKind::FunctionBlock) {
continue;
}
FbLayout lay;
lay.type_name = p.name;
for (const VarBlock& b : p.blocks) {
// 字段 = input / output / 内部 varexternal 与 global 不是字段
if (b.section == VarSection::External ||
b.section == VarSection::Global) {
continue;
}
for (const VarDecl& d : b.vars) {
if (!is_scalar(d.type.kind)) {
return fail(u.path,
"FB field type must be BOOL/INT/TIME ('" +
d.name + "')",
d.line, d.col);
}
FbField f;
f.name = d.name;
f.type = d.type.kind;
lay.fields.push_back(f);
}
}
out_->fb_types[p.name] = lay;
}
}
return true;
}
// ---- 3. 逐 POU 建局部符号并解析引用 ----
bool resolve_scopes() {
for (SourceUnit& u : const_cast<std::vector<SourceUnit>&>(units_)) {
for (const POU& p : u.ast.pous) {
LinkResult::PouScope& sc = scope_of(p.name);
if (!build_scope_syms(u.path, p, sc)) {
return false;
}
for (const Stmt& st : p.body) {
if (!check_stmt(u.path, sc, p, st)) {
return false;
}
}
}
}
return true;
}
LinkResult::PouScope& scope_of(const std::string& name) {
for (LinkResult::PouScope& sc : out_->scopes) {
if (sc.name == name) {
return sc;
}
}
static LinkResult::PouScope kEmpty;
return kEmpty;
}
bool build_scope_syms(const std::string& file, const POU& p,
LinkResult::PouScope& sc) {
for (const VarBlock& b : p.blocks) {
for (const VarDecl& d : b.vars) {
switch (b.section) {
case VarSection::Global:
return fail(file, "VAR_GLOBAL only allowed in gvl file",
d.line, d.col);
case VarSection::External: {
const auto it = out_->global_index.find(d.name);
if (it == out_->global_index.end()) {
return fail(file, "undeclared external '" + d.name + "'",
d.line, d.col);
}
const Symbol& g = out_->globals[it->second];
if (g.type_name != type_str(d.type)) {
return fail(file, "external type mismatch for '" + d.name + "'",
d.line, d.col);
}
Symbol s;
s.kind = SymbolKind::External;
s.name = d.name;
s.type_name = g.type_name;
s.address = g.address;
s.source_file = file;
s.line = d.line;
s.col = d.col;
sc.syms.push_back(s);
break;
}
default: {
const bool is_fb = d.type.kind == TypeKind::FbUser ||
d.type.kind == TypeKind::FbBuiltin;
Symbol s;
s.kind = is_fb ? SymbolKind::FbInstance
: (b.section == VarSection::Input
? SymbolKind::Input
: b.section == VarSection::Output
? SymbolKind::Output
: SymbolKind::Local);
s.name = d.name;
s.type_name = type_str(d.type);
s.source_file = file;
s.line = d.line;
s.col = d.col;
sc.syms.push_back(s);
if (is_fb) {
FbLayout lay;
if (d.type.kind == TypeKind::FbBuiltin) {
const FbLayout* bl = builtin_layout(d.type.name);
if (!bl) {
return fail(file, "unknown builtin FB '" + d.type.name + "'",
d.line, d.col);
}
lay = *bl;
} else {
const auto it = out_->fb_types.find(d.type.name);
if (it == out_->fb_types.end()) {
return fail(file, "unknown FB type '" + d.type.name + "'",
d.line, d.col);
}
lay = it->second;
}
sc.fb_instances[d.name] = lay;
}
break;
}
}
}
}
return true;
}
// ---- 引用解析 ----
const Symbol* find_sym(const LinkResult::PouScope& sc,
const std::string& name) const {
for (const Symbol& s : sc.syms) {
if (s.name == name) {
return &s;
}
}
return nullptr;
}
bool check_stmt(const std::string& file, const LinkResult::PouScope& sc,
const POU& pou, const Stmt& st) {
switch (st.kind) {
case StmtKind::Assign: {
// FUNCTION 内对函数名赋值 = 结果值写入(用例 13 约定)
const bool is_result_assign =
pou.kind == PouKind::Function && st.target == pou.name;
if (!is_result_assign && find_sym(sc, st.target) == nullptr &&
out_->global_index.count(st.target) == 0) {
return fail(file, "undeclared identifier '" + st.target + "'",
0, 0);
}
return check_expr(file, sc, *st.value);
}
case StmtKind::FbCall: {
const Symbol* inst = find_sym(sc, st.instance);
if (inst == nullptr) {
return fail(file, "undeclared FB instance '" + st.instance + "'",
0, 0);
}
if (inst->kind != SymbolKind::FbInstance) {
return fail(file, "'" + st.instance + "' is not an FB instance",
0, 0);
}
const auto it = sc.fb_instances.find(st.instance);
const FbLayout& lay = it->second;
for (const FbArg& a : st.args) {
bool found = false;
for (const FbField& f : lay.fields) {
if (f.name == a.name) {
found = true;
break;
}
}
if (!found) {
return fail(file,
"unknown input '" + a.name + "' for FB '" +
st.instance + "'",
0, 0);
}
if (!check_expr(file, sc, *a.value)) {
return false;
}
}
return true;
}
case StmtKind::If:
if (!check_expr(file, sc, *st.cond)) {
return false;
}
for (const Stmt& s : st.body) {
if (!check_stmt(file, sc, pou, s)) return false;
}
for (const IfBranch& b : st.elsifs) {
if (!check_expr(file, sc, *b.cond)) return false;
for (const Stmt& s : b.body) {
if (!check_stmt(file, sc, pou, s)) return false;
}
}
for (const Stmt& s : st.else_body) {
if (!check_stmt(file, sc, pou, s)) return false;
}
return true;
case StmtKind::While:
if (!check_expr(file, sc, *st.cond)) {
return false;
}
for (const Stmt& s : st.body) {
if (!check_stmt(file, sc, pou, s)) return false;
}
return true;
}
return true;
}
bool check_expr(const std::string& file, const LinkResult::PouScope& sc,
const Expr& e) {
switch (e.kind) {
case ExprKind::VarRef:
if (find_sym(sc, e.name) == nullptr &&
out_->global_index.count(e.name) == 0) {
return fail(file, "undeclared identifier '" + e.name + "'", 0, 0);
}
return true;
case ExprKind::Field: {
const Symbol* inst = find_sym(sc, e.name);
if (inst == nullptr || inst->kind != SymbolKind::FbInstance) {
return fail(file, "undeclared FB instance '" + e.name + "'", 0, 0);
}
const auto it = sc.fb_instances.find(e.name);
bool found = false;
for (const FbField& f : it->second.fields) {
if (f.name == e.field) {
found = true;
break;
}
}
if (!found) {
return fail(file, "unknown field '" + e.field + "' for FB '" +
e.name + "'",
0, 0);
}
return true;
}
case ExprKind::Call: {
PouKind k = PouKind::Program;
if (!find_function(e.name, &k)) {
return fail(file, "undeclared function '" + e.name + "'", 0, 0);
}
if (k != PouKind::Function) {
return fail(file, "'" + e.name + "' is not a function", 0, 0);
}
call_edges_[sc.name].insert(e.name);
for (const auto& a : e.args) {
if (!check_expr(file, sc, *a)) return false;
}
return true;
}
default:
break;
}
if (e.lhs && !check_expr(file, sc, *e.lhs)) return false;
if (e.rhs && !check_expr(file, sc, *e.rhs)) return false;
if (e.operand && !check_expr(file, sc, *e.operand)) return false;
return true;
}
bool find_function(const std::string& name, PouKind* kind) const {
for (const LinkResult::PouScope& sc : out_->scopes) {
if (sc.name == name) {
*kind = sc.kind;
return true;
}
}
return false;
}
// ---- 4. 循环调用检测(函数依赖图 DFS)----
bool check_cycles() {
std::set<std::string> visiting;
std::set<std::string> done;
for (const auto& [fn, _] : call_edges_) {
if (!dfs(fn, visiting, done)) {
return false;
}
}
return true;
}
bool dfs(const std::string& fn, std::set<std::string>& visiting,
std::set<std::string>& done) {
if (done.count(fn)) {
return true;
}
if (visiting.count(fn)) {
return fail("", "recursive call involving '" + fn + "'", 0, 0);
}
visiting.insert(fn);
const auto it = call_edges_.find(fn);
if (it != call_edges_.end()) {
for (const std::string& callee : it->second) {
if (!dfs(callee, visiting, done)) {
return false;
}
}
}
visiting.erase(fn);
done.insert(fn);
return true;
}
// ---- 5. io 校验 ----
bool check_io() {
for (const isa::IoBinding& b : proj_.io) {
std::string key = b.var;
for (char& ch : key) {
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
}
if (out_->global_index.count(key) == 0) {
return fail(proj_.name + "/project.toml",
"io.var '" + b.var + "' not in GVL", 0, 0);
}
}
return true;
}
// ---- 6. 入口 ----
bool has_program_main() const {
for (const LinkResult::PouScope& sc : out_->scopes) {
if (sc.kind == PouKind::Program && sc.name == "main") {
return true;
}
}
return false;
}
const Project& proj_;
const std::vector<SourceUnit>& units_;
LinkResult* out_;
std::string* err_;
std::filesystem::path gvl_path_;
std::map<std::string, std::set<std::string>> call_edges_;
};
} // namespace
bool link_project(const Project& proj, const std::vector<SourceUnit>& units,
LinkResult* out, std::string* err) {
out->globals.clear();
out->global_index.clear();
out->fb_types.clear();
out->scopes.clear();
out->fn_order.clear();
Linker l(proj, units, out, err);
return l.run();
}
bool load_unit(const std::string& path, SourceUnit* out, std::string* err) {
out->path = path;
return parse_pous_file(path, &out->ast, err);
}
std::string dump_symbols(const LinkResult& r) {
std::string out;
for (const Symbol& s : r.globals) {
out += s.name + " " + kind_name(s.kind) + " " + s.type_name + " " +
std::to_string(s.address) + " " + s.source_file + "\n";
}
for (const LinkResult::PouScope& sc : r.scopes) {
out += "[" + sc.name + "]\n";
for (const Symbol& s : sc.syms) {
out += s.name + " " + kind_name(s.kind) + " " + s.type_name + " " +
std::to_string(s.address) + " " + s.source_file + "\n";
}
}
return out;
}
} // namespace compiler
+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;
}