实现 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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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。失败返回 false,err 前缀 "type error"。
|
||||
bool check_project(const Project& proj, const std::vector<SourceUnit>& units,
|
||||
const LinkResult& link, std::string* err);
|
||||
}
|
||||
@@ -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: <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 ≻
|
||||
}
|
||||
}
|
||||
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<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
|
||||
Reference in New Issue
Block a user