Compare commits

...
2 Commits
Author SHA1 Message Date
Admin 5c140c36ff Merge branch 'dev_TypeChecking' into dev 2026-08-21 11:27:10 +08:00
Admin 9cd4997a3a 实现 12.7 类型检查:表达式类型、语句规则、FUNCTION 禁写全局。
- 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
2026-08-21 11:26:55 +08:00
7 changed files with 826 additions and 1 deletions
+78
View File
@@ -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. 用例 14FUNCTION 写全局)→ `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` + 文档
+2
View File
@@ -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,跑扫描周期 |
+2 -1
View File
@@ -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)
+26
View File
@@ -0,0 +1,26 @@
/**
* @file Typecheck.h
* @brief 类型检查
* @author
* @date 2026-08-21
*/
#pragma once
#include <string>
#include <vector>
#include "compiler/Linker.h"
#include "compiler/Parser.h"
#include "compiler/Project.h"
namespace compiler {
// 表达式求值类型(v1 三型,无隐式宽化)
enum class TType { Bool, Int, Time };
// 对工程做类型检查(在链接成功后调用)。
// 规则见 Doc/compiler/类型检查.md。失败返回 falseerr 前缀 "type error"。
bool check_project(const Project& proj, const std::vector<SourceUnit>& units,
const LinkResult& link, std::string* err);
}
+494
View File
@@ -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 → TTypeFB 类型返回 false
* - to_type(string) 符号 type_name 字符串 → TType
* - Checker::Checker (构造)存工程/源文件/链接结果/错误输出
* - Checker::run 逐 POU 逐语句检查
* - Checker::fail 组装 "type error: <msg> (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 <cstdio>
#include <string>
#include <vector>
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<SourceUnit>& 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: <msg> (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 &sc;
}
}
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 合法;falseerr 已写)
*/
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 条件表达式必须为 BOOLIF / WHILE 共用)
* @param file 出错文件
* @param sc 当前作用域
* @param e 条件表达式
* @return true 合法;falseerr 已写)
*/
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 合法;falseerr 已写)
*/
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 合法;falseerr 已写)
*/
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<SourceUnit>& 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<SourceUnit>& units,
const LinkResult& link, std::string* err) {
Checker c(proj, units, link, err);
return c.run();
}
} // namespace compiler
+11
View File
@@ -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)
+213
View File
@@ -0,0 +1,213 @@
/**
* @file typecheck_test.cpp
* @brief 类型检查测试:用例 14 + 负例 + 正例
* @author
* @date 2026-08-21
*/
#include <cstdio>
#include <filesystem>
#include <string>
#include <vector>
#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<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));
}
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<SourceUnit> 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. 用例 14FUNCTION 写全局(经 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;
}