From 6f02c18bbb9b984bdaf6109cd26c631487b6ed05 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:14:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=2012.6=20=E7=AC=A6=E5=8F=B7?= =?UTF-8?q?=E8=A1=A8=E4=B8=8E=E9=93=BE=E6=8E=A5=EF=BC=9A=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E5=AE=9A=E6=A7=BD=E3=80=81=E5=BC=95=E7=94=A8=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E3=80=81=E5=BE=AA=E7=8E=AF=E6=A3=80=E6=B5=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 按期望拒 --- Doc/compiler/符号表与链接.md | 111 ++++++ Doc/索引.md | 2 + compiler/CMakeLists.txt | 3 +- compiler/include/compiler/Linker.h | 82 +++++ compiler/src/Linker.cpp | 548 +++++++++++++++++++++++++++++ tests/CMakeLists.txt | 11 + tests/src/linker_test.cpp | 229 ++++++++++++ 7 files changed, 985 insertions(+), 1 deletion(-) create mode 100644 Doc/compiler/符号表与链接.md create mode 100644 compiler/include/compiler/Linker.h create mode 100644 compiler/src/Linker.cpp create mode 100644 tests/src/linker_test.cpp diff --git a/Doc/compiler/符号表与链接.md b/Doc/compiler/符号表与链接.md new file mode 100644 index 0000000..f540ed9 --- /dev/null +++ b/Doc/compiler/符号表与链接.md @@ -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 局部暂 0(12.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` + 文档 diff --git a/Doc/索引.md b/Doc/索引.md index c96b7b9..e4a511d 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -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,跑扫描周期 | diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index 7bab162..40a8484 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -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) diff --git a/compiler/include/compiler/Linker.h b/compiler/include/compiler/Linker.h new file mode 100644 index 0000000..34b4976 --- /dev/null +++ b/compiler/include/compiler/Linker.h @@ -0,0 +1,82 @@ +/** + * @file Linker.h + * @brief 符号表与链接 + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include +#include +#include + +#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);局部暂 0(12.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 fields; // 段序:input → output → 内部 var + }; + + // 一个源文件的 AST(供链接输入) + struct SourceUnit { + std::string path; + Unit ast; + }; + + // 链接结果 + struct LinkResult { + // 全局槽表:下标即槽号(声明顺序) + std::vector globals; + std::map global_index; // 名 → 槽号 + + // 用户 FB 类型布局(按类型名) + std::map fb_types; + + // 每 POU 的局部符号与 FB 实例 + struct PouScope { + std::string name; + PouKind kind = PouKind::Program; + std::vector syms; // local/input/output/external/fb_instance + std::map fb_instances; // 实例名 → 布局 + }; + std::vector scopes; + + // 函数表顺序(PROGRAM / FUNCTION / FUNCTION_BLOCK 名,收集序) + std::vector fn_order; + }; + + // 链接一个工程:校验 GVL、定全局槽、解析 VAR_EXTERNAL、FB 实例与字段、 + // 函数循环调用检测、io 校验。失败返回 false,err 前缀 "link error"。 + bool link_project(const Project& proj, const std::vector& 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); +} diff --git a/compiler/src/Linker.cpp b/compiler/src/Linker.cpp new file mode 100644 index 0000000..c3ebaa3 --- /dev/null +++ b/compiler/src/Linker.cpp @@ -0,0 +1,548 @@ +/** + * @file Linker.cpp + * @brief 符号表与链接 + * @author + * @date 2026-08-21 + */ + +#include "compiler/Linker.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +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& 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(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 / 内部 var;external 与 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&>(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 visiting; + std::set done; + for (const auto& [fn, _] : call_edges_) { + if (!dfs(fn, visiting, done)) { + return false; + } + } + return true; + } + + bool dfs(const std::string& fn, std::set& visiting, + std::set& 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(std::tolower(static_cast(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& units_; + LinkResult* out_; + std::string* err_; + std::filesystem::path gvl_path_; + std::map> call_edges_; + }; + +} // namespace + +bool link_project(const Project& proj, const std::vector& 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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ea1428a..afd924d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/src/linker_test.cpp b/tests/src/linker_test.cpp new file mode 100644 index 0000000..b832389 --- /dev/null +++ b/tests/src/linker_test.cpp @@ -0,0 +1,229 @@ +/** + * @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; +}