实现 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:
@@ -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)
|
||||
|
||||
@@ -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. 用例 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;
|
||||
}
|
||||
Reference in New Issue
Block a user