From 9cd4997a3a7cb886cf63127db1cc50894b8c2895 Mon Sep 17 00:00:00 2001 From: chentianya Date: Fri, 21 Aug 2026 11:26:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=2012.7=20=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=EF=BC=9A=E8=A1=A8=E8=BE=BE=E5=BC=8F=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E3=80=81=E8=AF=AD=E5=8F=A5=E8=A7=84=E5=88=99=E3=80=81?= =?UTF-8?q?FUNCTION=20=E7=A6=81=E5=86=99=E5=85=A8=E5=B1=80=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Typecheck.h/cpp:三型(BOOL/INT/TIME)求值,AND/OR/NOT 只吃 BOOL、算术只吃 INT(TIME 无算术)、比较同型、IF/WHILE 条件必须 BOOL - FUNCTION 禁写全局(含经 VAR_EXTERNAL)——用例 14 点亮;函数名赋值=返回类型 - FB 命名实参匹配字段类型;FB 实例不能当值 - typecheck_test:23 断言(用例 14 + 8 类负例 + 13 个正例),ctest 8/8 --- Doc/compiler/类型检查.md | 78 ++++ Doc/索引.md | 2 + compiler/CMakeLists.txt | 3 +- compiler/include/compiler/Typecheck.h | 26 ++ compiler/src/Typecheck.cpp | 494 ++++++++++++++++++++++++++ tests/CMakeLists.txt | 11 + tests/src/typecheck_test.cpp | 213 +++++++++++ 7 files changed, 826 insertions(+), 1 deletion(-) create mode 100644 Doc/compiler/类型检查.md create mode 100644 compiler/include/compiler/Typecheck.h create mode 100644 compiler/src/Typecheck.cpp create mode 100644 tests/src/typecheck_test.cpp diff --git a/Doc/compiler/类型检查.md b/Doc/compiler/类型检查.md new file mode 100644 index 0000000..ed91df7 --- /dev/null +++ b/Doc/compiler/类型检查.md @@ -0,0 +1,78 @@ +# 类型检查(12.7) + +compiler 模块的类型规则检查。输入:`Project` + 各 `.st` 的 `Unit`(AST)+ `LinkResult`(符号已解析);输出:通过或 `type error`。全工程定案见 [`初步计划.md`](../初步计划.md) 12.7。 + +## 做 / 不做 + +**做** + +- `:=`、比较、算术的类型规则;`AND` / `OR` / `NOT` 只吃 `BOOL` +- `FUNCTION` 禁止写全局(含经 `VAR_EXTERNAL`)——用例 14 +- FB 调用:命名输入类型匹配;`fb.Q` 字段存在且类型对 +- 表达式求值类型:`BOOL` / `INT` / `TIME` 三型 +- 无隐式宽化;`INT` 与 `TIME` 不混用 + +**不做(第一版)** + +- 常量折叠、溢出区间检查(INT 字面量范围,饱和在 VM 层) +- `FUNCTION` 实参与形参的个数/类型核对(12.7 未要求,只核对 FB 命名输入) +- 变量初始化表达式的类型核对(语法层已限字面量) + +## 表达式求值类型 + +```cpp +enum class TType { Bool, Int, Time }; +``` + +| 节点 | 规则 | 结果 | +|---|---|---| +| `LitBool` / `LitInt` / `LitTime` | — | Bool / Int / Time | +| `VarRef` | 查符号(局部/输入/输出/外部/全局)类型 | 符号类型 | +| `Field` | 实例字段类型(布局已由 12.6 提供) | 字段类型 | +| `Not` | 操作数必须 Bool | Bool | +| `And` / `Or` | 两侧必须 Bool(短路已由语法层保留) | Bool | +| `Cmp` | 两侧同型(Bool/Int/Time 均可比较) | Bool | +| `Add/Sub/Mul/Div` | 两侧必须 Int(**TIME 无算术**,INT 与 TIME 不混用) | Int | +| `Neg` | 操作数必须 Int | Int | +| `Call` | 函数存在(12.6 已验);结果 = 函数返回类型 | 返回类型 | +| `VarRef` 指向 FB 实例 | 实例不能当值 | type error | + +## 语句规则 + +| 语句 | 规则 | +|---|---| +| `Assign`(普通) | 左值类型 == 表达式类型;左值可为局部/输入/输出/外部/全局 | +| `Assign`(函数名,FUNCTION 内) | 表达式类型 == 函数返回类型(用例 13 约定) | +| **`FUNCTION` 内左值是全局/外部** | **拒绝:`function cannot write global`(用例 14,含经 VAR_EXTERNAL)** | +| `FbCall` | 每个命名实参表达式类型 == 实例对应字段类型(布局 12.6 已校验存在性) | +| `If` / `While` | 条件表达式必须 Bool | + +## 错误 + +稳定前缀 `type error`,带文件与行列(尽量带): + +```text +type error: AND operands must be BOOL (main.st, line 5, col 9) +type error: type mismatch in assignment to 'q0_0' (INT vs BOOL) (main.st, line 6, col 13) +type error: function cannot write global 'g1' (main.st, line 3, col 5) +type error: TIME has no arithmetic (main.st, line 4, col 12) +type error: FB input 'pt' expects TIME (main.st, line 5, col 16) +``` + +## 完成标准 + +1. 用例 14(FUNCTION 写全局)→ `type error`(12.6 已放行,本阶段点亮) +2. line1 / 用例 07 / 13 / 15 / 17 等合法工程类型检查通过 +3. 负例:AND 吃 INT、赋值类型不匹配、INT+TIME、NOT INT、FB 输入类型不匹配、比较异型 → `type error` +4. 全部构建 + `ctest` 无回归 + +--- + +## 执行计划(单步确认) + +1. **`Typecheck.h`(新建 `compiler/include/compiler/Typecheck.h`)**:`TType` + `check_project` 声明 +2. **`Typecheck.cpp` 前半**:符号类型解析(VarRef/Field/Call/字面量)+ 表达式递归定类型 +3. **`Typecheck.cpp` 后半**:语句规则(Assign 含函数名/FUNCTION 禁写全局、FbCall 实参、IF/WHILE 条件) +4. **`typecheck_test`(`tests/src/typecheck_test.cpp` + CMake)**:用例 14 + 负例 + 正例 +5. **验证**:`cmake --build` + `ctest`(新增 `typecheck_types` 后 8/8 全绿) +6. **提交**:`Typecheck.h/cpp` + `typecheck_test` + 文档 diff --git a/Doc/索引.md b/Doc/索引.md index e4a511d..4eaf3af 100644 --- a/Doc/索引.md +++ b/Doc/索引.md @@ -11,6 +11,7 @@ doc/ compiler/词法.md compiler/语法.md compiler/符号表与链接.md + compiler/类型检查.md vm/扫描周期.md executor/执行器入口.md ``` @@ -23,6 +24,7 @@ doc/ | [`compiler/词法.md`](compiler/词法.md) | 12.4 词法:关键字表、token、大小写、TIME 字面量 | | [`compiler/语法.md`](compiler/语法.md) | 12.5 语法:AST 结构、文法、拒绝清单 | | [`compiler/符号表与链接.md`](compiler/符号表与链接.md) | 12.6 符号表与链接:数据区定址、FB 布局、错误类别 | +| [`compiler/类型检查.md`](compiler/类型检查.md) | 12.7 类型检查:表达式类型、语句规则、错误类别 | | [`vm/扫描周期.md`](vm/扫描周期.md) | 扫描周期与 VM 边界 | | [`executor/执行器入口.md`](executor/执行器入口.md) | 可执行入口:加载 `.stb` + sidecar,跑扫描周期 | diff --git a/compiler/CMakeLists.txt b/compiler/CMakeLists.txt index 40a8484..efcffe0 100644 --- a/compiler/CMakeLists.txt +++ b/compiler/CMakeLists.txt @@ -11,7 +11,8 @@ add_library(compiler STATIC ./src/Lexer.cpp ./src/Project.cpp ./src/Parser.cpp - ./src/Linker.cpp) + ./src/Linker.cpp + ./src/Typecheck.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/Typecheck.h b/compiler/include/compiler/Typecheck.h new file mode 100644 index 0000000..0f40ce9 --- /dev/null +++ b/compiler/include/compiler/Typecheck.h @@ -0,0 +1,26 @@ +/** + * @file Typecheck.h + * @brief 类型检查 + * @author + * @date 2026-08-21 + */ + +#pragma once + +#include +#include + +#include "compiler/Linker.h" +#include "compiler/Parser.h" +#include "compiler/Project.h" + +namespace compiler { + + // 表达式求值类型(v1 三型,无隐式宽化) + enum class TType { Bool, Int, Time }; + + // 对工程做类型检查(在链接成功后调用)。 + // 规则见 Doc/compiler/类型检查.md。失败返回 false,err 前缀 "type error"。 + bool check_project(const Project& proj, const std::vector& units, + const LinkResult& link, std::string* err); +} diff --git a/compiler/src/Typecheck.cpp b/compiler/src/Typecheck.cpp new file mode 100644 index 0000000..bcee19f --- /dev/null +++ b/compiler/src/Typecheck.cpp @@ -0,0 +1,494 @@ +/** + * @file Typecheck.cpp + * @brief 类型检查 + * @author + * @date 2026-08-21 + * + * @details 设计说明(详见 Doc/compiler/类型检查.md): + * - 在链接成功后运行:符号/字段/函数存在性已由 12.6 保证,本阶段只查类型 + * - 表达式求值类型三型:BOOL / INT / TIME;无隐式宽化,INT 与 TIME 不混用 + * - 语句规则:赋值类型相等、FUNCTION 禁写全局(含 EXTERNAL,用例 14)、 + * FB 命名实参匹配字段类型、IF/WHILE 条件必须 BOOL + * - 错误:稳定前缀 "type error" + 文件(Expr/Stmt 无行列信息,只带文件) + * + * 函数清单: + * - tname TType → 文本名(BOOL/INT/TIME) + * - to_type(TypeKind) TypeKind → TType(FB 类型返回 false) + * - to_type(string) 符号 type_name 字符串 → TType + * - Checker::Checker (构造)存工程/源文件/链接结果/错误输出 + * - Checker::run 逐 POU 逐语句检查 + * - Checker::fail 组装 "type error: (file)" 返回 false + * - find_scope 按 POU 名取作用域 + * - find_sym 在作用域里按名查符号 + * - find_global 按名查全局符号 + * - find_pou 按名查 POU AST(函数返回类型用) + * - check_stmt 语句规则分发(赋值/FB 调用/IF/WHILE) + * - assign_target_type 解析赋值左值类型(局部/全局/函数名),FUNCTION 禁写全局 + * - check_expr 表达式定类型(递归) + * - check_project 对外入口:逐 POU 检查 + */ + +#include "compiler/Typecheck.h" + +#include +#include +#include + +namespace compiler { +namespace { + + /** + * @brief TType → 文本名(错误报文用) + * @param t 求值类型 + * @return "BOOL" / "INT" / "TIME" + */ + const char* tname(TType t) { + switch (t) { + case TType::Bool: return "BOOL"; + case TType::Int: return "INT"; + case TType::Time: return "TIME"; + } + return "?"; + } + + /** + * @brief TypeKind → TType + * @param k 类型种类 + * @param out 输出求值类型 + * @return true 标量(BOOL/INT/TIME);FB 类型返回 false + */ + bool to_type(TypeKind k, TType* out) { + switch (k) { + case TypeKind::Bool: *out = TType::Bool; return true; + case TypeKind::Int: *out = TType::Int; return true; + case TypeKind::Time: *out = TType::Time; return true; + default: return false; + } + } + + /** + * @brief 符号 type_name 字符串 → TType + * @param name "bool" / "int" / "time"(小写) + * @param out 输出求值类型 + * @return true 标量;FB 类型名返回 false + */ + bool to_type(const std::string& name, TType* out) { + if (name == "bool") { *out = TType::Bool; return true; } + if (name == "int") { *out = TType::Int; return true; } + if (name == "time") { *out = TType::Time; return true; } + return false; + } + + /** + * @brief 类型检查器 + * + * @details 链接结果只读;不修改 AST 与符号表。 + */ + class Checker { + public: + /** + * @brief 构造检查器 + * @param proj 工程定义(未用,保留接口对称;io 类型映射留待 12.8) + * @param units 全部源文件的 AST + * @param link 链接结果(符号/布局已解析) + * @param err 错误输出;可为 nullptr(静默) + */ + Checker(const Project& proj, const std::vector& units, + const LinkResult& link, std::string* err) + : proj_(proj), units_(units), link_(link), err_(err) {} + + /** + * @brief 逐 POU 逐语句做类型检查 + * @return true 全部通过;false 首个类型错误(err 已写) + */ + bool run() { + for (const SourceUnit& u : units_) { + for (const POU& p : u.ast.pous) { + const LinkResult::PouScope* sc = find_scope(p.name); + if (sc == nullptr) { + continue; // 不应发生(12.6 已登记) + } + for (const Stmt& st : p.body) { + if (!check_stmt(u.path, *sc, p, st)) { + return false; + } + } + } + } + return true; + } + + private: + /** + * @brief 组装 "type error: (file)" 写入 err + * @details Expr/Stmt 不携带行列,只报文件;err_ 为 nullptr 时静默 + * @param file 出错文件 + * @param msg 错误描述(不含前缀) + * @return 恒 false(便于 return fail(...) 一行退出) + */ + bool fail(const std::string& file, const std::string& msg) { + if (err_) { + *err_ = "type error: " + msg + " (" + file + ")"; + } + return false; + } + + /** + * @brief 按 POU 名取作用域 + * @param name POU 名(小写) + * @return 作用域指针;未找到返回 nullptr + */ + const LinkResult::PouScope* find_scope(const std::string& name) const { + for (const LinkResult::PouScope& sc : link_.scopes) { + if (sc.name == name) { + return ≻ + } + } + return nullptr; + } + + /** + * @brief 在作用域里按名查符号 + * @param sc 作用域 + * @param name 符号名(小写) + * @return 符号指针;未找到返回 nullptr + */ + 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; + } + + /** + * @brief 按名查全局符号 + * @param name 全局名(小写) + * @return 符号指针;未找到返回 nullptr + */ + const Symbol* find_global(const std::string& name) const { + const auto it = link_.global_index.find(name); + if (it == link_.global_index.end()) { + return nullptr; + } + return &link_.globals[it->second]; + } + + /** + * @brief 按名查 POU AST(函数返回类型用) + * @param name POU 名(小写) + * @return POU 指针;未找到返回 nullptr + */ + 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; + } + + /** + * @brief 语句规则分发 + * @param file 出错文件 + * @param sc 当前作用域 + * @param pou 当前 POU(函数名赋值 / 禁写全局判定用) + * @param st 语句 AST + * @return true 合法;false(err 已写) + */ + bool check_stmt(const std::string& file, const LinkResult::PouScope& sc, + const POU& pou, const Stmt& st) { + switch (st.kind) { + case StmtKind::Assign: { + TType lhs; + if (!assign_target_type(file, sc, pou, st.target, &lhs)) { + return false; + } + TType rhs; + if (!check_expr(file, sc, *st.value, &rhs)) { + return false; + } + if (lhs != rhs) { + return fail(file, "type mismatch in assignment to '" + st.target + + "' (" + tname(lhs) + " vs " + tname(rhs) + ")"); + } + return true; + } + case StmtKind::FbCall: { + const auto it = sc.fb_instances.find(st.instance); + if (it == sc.fb_instances.end()) { + return fail(file, "no layout for FB instance '" + st.instance + "'"); + } + for (const FbArg& a : st.args) { + TType want = TType::Bool; + bool found = false; + for (const FbField& f : it->second.fields) { + if (f.name == a.name) { + found = to_type(f.type, &want); + break; + } + } + if (!found) { + return fail(file, "unknown input '" + a.name + "' for FB '" + + st.instance + "'"); + } + TType got; + if (!check_expr(file, sc, *a.value, &got)) { + return false; + } + if (got != want) { + return fail(file, "FB input '" + a.name + "' expects " + + tname(want)); + } + } + return true; + } + case StmtKind::If: + if (!check_cond(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_cond(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_cond(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; + } + + /** + * @brief 条件表达式必须为 BOOL(IF / WHILE 共用) + * @param file 出错文件 + * @param sc 当前作用域 + * @param e 条件表达式 + * @return true 合法;false(err 已写) + */ + bool check_cond(const std::string& file, const LinkResult::PouScope& sc, + const Expr& e) { + TType t; + if (!check_expr(file, sc, e, &t)) { + return false; + } + if (t != TType::Bool) { + return fail(file, "condition must be BOOL, got " + std::string(tname(t))); + } + return true; + } + + /** + * @brief 解析赋值左值类型 + * @details 普通左值:局部/输入/输出/外部/全局 → 符号类型; + * FUNCTION 内函数名 → 返回类型;**FUNCTION 内左值是全局/外部 → 拒绝** + * (用例 14:FUNCTION 禁止写全局,含经 VAR_EXTERNAL) + * @param file 出错文件 + * @param sc 当前作用域 + * @param pou 当前 POU + * @param target 左值标识符 + * @param out 输出左值类型 + * @return true 合法;false(err 已写) + */ + bool assign_target_type(const std::string& file, + const LinkResult::PouScope& sc, const POU& pou, + const std::string& target, TType* out) { + // FUNCTION 内对函数名赋值 = 结果值写入(用例 13 约定) + if (pou.kind == PouKind::Function && target == pou.name) { + return to_type(pou.result_type.kind, out) || + fail(file, "function result must be scalar"); + } + + const Symbol* s = find_sym(sc, target); + if (s == nullptr) { + s = find_global(target); + } + if (s == nullptr) { + return fail(file, "undeclared identifier '" + target + "'"); + } + + // FUNCTION 禁写全局(含经 VAR_EXTERNAL) + if (pou.kind == PouKind::Function && + (s->kind == SymbolKind::Global || s->kind == SymbolKind::External)) { + return fail(file, "function cannot write global '" + target + "'"); + } + + if (!to_type(s->type_name, out)) { + return fail(file, "'" + target + "' has no scalar type"); + } + return true; + } + + /** + * @brief 表达式定类型(递归) + * @param file 出错文件 + * @param sc 当前作用域 + * @param e 表达式 AST + * @param out 输出求值类型 + * @return true 合法;false(err 已写) + */ + bool check_expr(const std::string& file, const LinkResult::PouScope& sc, + const Expr& e, TType* out) { + switch (e.kind) { + case ExprKind::LitBool: + *out = TType::Bool; + return true; + case ExprKind::LitInt: + *out = TType::Int; + return true; + case ExprKind::LitTime: + *out = TType::Time; + return true; + case ExprKind::VarRef: { + const Symbol* s = find_sym(sc, e.name); + if (s == nullptr) { + s = find_global(e.name); + } + if (s == nullptr) { + return fail(file, "undeclared identifier '" + e.name + "'"); + } + if (s->kind == SymbolKind::FbInstance) { + return fail(file, "FB instance '" + e.name + "' cannot be used as a value"); + } + if (!to_type(s->type_name, out)) { + return fail(file, "'" + e.name + "' has no scalar type"); + } + return true; + } + case ExprKind::Field: { + const auto it = sc.fb_instances.find(e.name); + if (it == sc.fb_instances.end()) { + return fail(file, "no layout for FB instance '" + e.name + "'"); + } + for (const FbField& f : it->second.fields) { + if (f.name == e.field) { + return to_type(f.type, out) || + fail(file, "field '" + e.field + "' has no scalar type"); + } + } + return fail(file, "unknown field '" + e.field + "' for FB '" + e.name + "'"); + } + case ExprKind::Not: { + TType t; + if (!check_expr(file, sc, *e.operand, &t)) { + return false; + } + if (t != TType::Bool) { + return fail(file, "NOT operand must be BOOL"); + } + *out = TType::Bool; + return true; + } + case ExprKind::And: + case ExprKind::Or: { + TType l, r; + if (!check_expr(file, sc, *e.lhs, &l) || !check_expr(file, sc, *e.rhs, &r)) { + return false; + } + if (l != TType::Bool || r != TType::Bool) { + const char* op = (e.kind == ExprKind::And) ? "AND" : "OR"; + return fail(file, std::string(op) + " operands must be BOOL"); + } + *out = TType::Bool; + return true; + } + case ExprKind::Cmp: { + TType l, r; + if (!check_expr(file, sc, *e.lhs, &l) || !check_expr(file, sc, *e.rhs, &r)) { + return false; + } + if (l != r) { + return fail(file, "comparison of mismatched types (" + + std::string(tname(l)) + " vs " + + std::string(tname(r)) + ")"); + } + *out = TType::Bool; + return true; + } + case ExprKind::Add: + case ExprKind::Sub: + case ExprKind::Mul: + case ExprKind::Div: { + TType l, r; + if (!check_expr(file, sc, *e.lhs, &l) || !check_expr(file, sc, *e.rhs, &r)) { + return false; + } + if (l != TType::Int || r != TType::Int) { + return fail(file, "arithmetic operands must be INT (TIME has no arithmetic)"); + } + *out = TType::Int; + return true; + } + case ExprKind::Neg: { + TType t; + if (!check_expr(file, sc, *e.operand, &t)) { + return false; + } + if (t != TType::Int) { + return fail(file, "unary minus operand must be INT"); + } + *out = TType::Int; + return true; + } + case ExprKind::Call: { + const POU* f = find_pou(e.name); + if (f == nullptr) { + return fail(file, "undeclared function '" + e.name + "'"); + } + for (const auto& a : e.args) { + TType at; + if (!check_expr(file, sc, *a, &at)) { + return false; + } + } + if (!to_type(f->result_type.kind, out)) { + return fail(file, "function '" + e.name + "' result must be scalar"); + } + return true; + } + } + return fail(file, "unsupported expression"); + } + + // ---- 成员 ---- + const Project& proj_; // 工程定义(本阶段未用) + const std::vector& units_; // 全部源文件 AST + const LinkResult& link_; // 链接结果(只读) + std::string* err_; // 错误输出(可空) + }; + +} // namespace + +/** + * @brief 对工程做类型检查(对外入口,链接成功后调用) + * @details 逐 POU 逐语句检查;规则见 Doc/compiler/类型检查.md + * @param proj 工程定义(本阶段未用,保留接口对称) + * @param units 全部源文件的 AST + * @param link 链接结果(符号/布局已解析) + * @param err 错误输出;可为 nullptr(静默) + * @return true 全部通过;false 失败(err 前缀 "type error") + */ +bool check_project(const Project& proj, const std::vector& units, + const LinkResult& link, std::string* err) { + Checker c(proj, units, link, err); + return c.run(); +} + +} // namespace compiler diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index afd924d..204615e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -56,3 +56,14 @@ target_compile_definitions(linker_test PRIVATE add_test(NAME linker_links COMMAND linker_test) + +# 类型检查测试:用例 14 + 负例 + 正例(REPO_ROOT 注入源目录绝对路径) +add_executable(typecheck_test + ./src/typecheck_test.cpp) + +target_link_libraries(typecheck_test PRIVATE compiler) +target_compile_definitions(typecheck_test PRIVATE + REPO_ROOT="${CMAKE_SOURCE_DIR}") + +add_test(NAME typecheck_types + COMMAND typecheck_test) diff --git a/tests/src/typecheck_test.cpp b/tests/src/typecheck_test.cpp new file mode 100644 index 0000000..e680967 --- /dev/null +++ b/tests/src/typecheck_test.cpp @@ -0,0 +1,213 @@ +/** + * @file typecheck_test.cpp + * @brief 类型检查测试:用例 14 + 负例 + 正例 + * @author + * @date 2026-08-21 + */ + +#include +#include +#include +#include + +#include "compiler/Linker.h" +#include "compiler/Project.h" +#include "compiler/Typecheck.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 check_case(const char* dir, compiler::LinkResult* r, 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)); + } + if (!link_project(p, units, r, err)) { + return false; + } + return check_project(p, units, *r, err); +} + +// 从内联源码构造工程(写临时目录),加载、链接、类型检查 +static bool check_src(const char* src_name, const char* st_content, + std::string* err) { + using namespace compiler; + const std::string dir = std::string(REPO_ROOT) + "/build/tc_tmp_" + src_name; + std::filesystem::create_directories(dir); + const std::string toml_path = dir + "/project.toml"; + const std::string st_path = dir + "/main.st"; + std::FILE* f = std::fopen(toml_path.c_str(), "w"); + std::fprintf(f, "[project]\nname = \"tc\"\nentry = \"program MAIN\"\n" + "cycle_limit = 1000\ndt_ms = 10\n" + "[files]\nst = [\"main.st\"]\n"); + std::fclose(f); + f = std::fopen(st_path.c_str(), "w"); + std::fputs(st_content, f); + std::fclose(f); + + Project p; + if (!parse_project(toml_path, &p, err)) { + return false; + } + std::vector units; + for (const std::string& file : compile_files(p)) { + SourceUnit u; + if (!load_unit(p.base_dir + "/" + file, &u, err)) { + return false; + } + units.push_back(std::move(u)); + } + LinkResult r; + if (!link_project(p, units, &r, err)) { + return false; + } + return check_project(p, units, r, err); +} + +static bool expect_type_err(const char* src_name, const char* st_content, + const char* keyword) { + std::string err; + if (check_src(src_name, st_content, &err)) { + std::printf("FAIL %s: typecheck passed\n", src_name); + return false; + } + if (err.find("type error") != 0) { + std::printf("FAIL %s: want 'type error', got '%s'\n", src_name, err.c_str()); + return false; + } + if (err.find(keyword) == std::string::npos) { + std::printf("FAIL %s: want '%s', got '%s'\n", src_name, keyword, err.c_str()); + return false; + } + ++g_checks; + return true; +} + +// ---- 1. 用例 14:FUNCTION 写全局(经 VAR_EXTERNAL)---- + +static bool test_case14() { + std::string err; + compiler::LinkResult r; + if (check_case("14_function_write_global", &r, &err)) { + std::printf("FAIL case14: typecheck passed\n"); + return false; + } + CHECK(err.find("type error") == 0); + CHECK(err.find("function cannot write global") != std::string::npos); + return true; +} + +// ---- 2. 负例 ---- + +static bool test_negative() { + // AND 吃 INT + if (!expect_type_err("and_int", + "PROGRAM MAIN\nVAR\n a, b : INT;\n x : BOOL;\nEND_VAR\n" + " x := a AND b;\nEND_PROGRAM\n", + "AND operands must be BOOL")) { + return false; + } + // 赋值类型不匹配(INT 赋给 BOOL) + if (!expect_type_err("assign_mismatch", + "PROGRAM MAIN\nVAR\n b : BOOL;\nEND_VAR\n" + " b := 5;\nEND_PROGRAM\n", + "type mismatch in assignment to 'b'")) { + return false; + } + // INT 与 TIME 混用算术 + if (!expect_type_err("int_time", + "PROGRAM MAIN\nVAR\n x : INT;\n t : TIME;\n y : INT;\nEND_VAR\n" + " y := x + t;\nEND_PROGRAM\n", + "TIME has no arithmetic")) { + return false; + } + // NOT 吃 INT + if (!expect_type_err("not_int", + "PROGRAM MAIN\nVAR\n a : INT;\n b : BOOL;\nEND_VAR\n" + " b := NOT a;\nEND_PROGRAM\n", + "NOT operand must be BOOL")) { + return false; + } + // 比较异型(INT = TIME) + if (!expect_type_err("cmp_mismatch", + "PROGRAM MAIN\nVAR\n x : INT;\n t : TIME;\n b : BOOL;\nEND_VAR\n" + " b := x = t;\nEND_PROGRAM\n", + "comparison of mismatched types")) { + return false; + } + // FB 输入类型不匹配(TON pt 期望 TIME,给 TRUE) + if (!expect_type_err("fb_arg", + "PROGRAM MAIN\nVAR\n t : TON;\n q : BOOL;\nEND_VAR\n" + " t(in := TRUE, pt := TRUE);\n" + " q := t.Q;\nEND_PROGRAM\n", + "FB input 'pt' expects TIME")) { + return false; + } + // IF 条件非 BOOL + if (!expect_type_err("if_cond", + "PROGRAM MAIN\nVAR\n n : INT;\n x : INT;\nEND_VAR\n" + " IF n THEN\n x := 1;\n END_IF\nEND_PROGRAM\n", + "condition must be BOOL")) { + return false; + } + // FB 实例当值用 + if (!expect_type_err("fb_value", + "PROGRAM MAIN\nVAR\n t : TON;\n b : BOOL;\nEND_VAR\n" + " b := t;\nEND_PROGRAM\n", + "cannot be used as a value")) { + return false; + } + return true; +} + +// ---- 3. 正例 ---- + +static bool test_positive() { + std::string err; + compiler::LinkResult r; + CHECK(check_case("01_empty_main", &r, &err)); + CHECK(check_case("02_bool_assign", &r, &err)); + CHECK(check_case("03_short_circuit", &r, &err)); + CHECK(check_case("04_if_elsif_else", &r, &err)); + CHECK(check_case("05_while_normal", &r, &err)); + CHECK(check_case("07_int_arith", &r, &err)); + CHECK(check_case("08_time_literal", &r, &err)); + CHECK(check_case("09_gvl_external", &r, &err)); + CHECK(check_case("13_function_call", &r, &err)); + CHECK(check_case("15_fb_instance", &r, &err)); + CHECK(check_case("17_ton", &r, &err)); + CHECK(check_case("18_tof_ctu", &r, &err)); + CHECK(check_case("20_line1", &r, &err)); + return true; +} + +int main() { + if (!test_case14()) return 1; + if (!test_negative()) return 1; + if (!test_positive()) return 1; + std::printf("typecheck_test: %d checks passed\n", g_checks); + return 0; +}