Lexer V2:
- 19 类型关键字(BOOL..DT);FB 名不再设关键字(IDENT)
- FLOAT_LIT(1.5/2.5e3)、进制前缀(2#/8#/16# 换算十进制)
- 日期字面量 D#/TOD#/DT#(历法换算 1970 纪元、闰年、值域检查)
Parser V2:
- TypeRef 字符串化(TypeKind{Scalar,FbBuiltin,FbUser},名字小写)
- ExprKind 增 LitReal/LitDate/LitTod/LitDt;Expr.double_value
- IDENT 类型统一 FbUser(内建判定在 Linker 查 fb 表)
Typecheck V2:
- TType 19 + Unresolved(字面量未定型)
- 禁隐式转换(赋值/算术/比较严格同型)
- resolve_to:未定型→消费点定型+值域检查(BOOL 0/1、无符号负数、
溢出、浮点仅 REAL/LREAL);算术按 kind 允许(TIME 仅 ±)
- Call 实参按函数参数类型;check_project 加 cfg
Linker V2:
- FbField.type_name 字符串化;内建 FB 布局查 machine.toml [[fb]] 表
(删 8 个硬编码布局,带缓存);link_project 加 cfg
Codegen V2:
- 浮点/日期字面量(LOADK func 8/4/4/6;LitReal 按 REAL 装载,LREAL TODO)
- 算术/比较 func 按操作数类型(expr_func)
- TypeKind 连锁适配
main.cpp:link/check 传 cfg;--disasm 头 22 字段逐行中文注释(L5)
+ slots 节;StbView 补 offset_const/offset_funcs
测试适配:parser/linker/typecheck/vm/cases 适配新接口与 V2 语义。
ctest 11/13(vm_cycles/cases_all 待阶段 4)。codegen_test 仍剥离(阶段 3-2 重写)。
241 lines
7.7 KiB
C++
241 lines
7.7 KiB
C++
/**
|
||
* @file linker_test.cpp
|
||
* @brief 链接测试:用例 09/10/11/12/16/19 + line1 正例
|
||
* @author
|
||
* @date 2026-08-21
|
||
*/
|
||
|
||
#include <cstdio>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#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<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));
|
||
}
|
||
compiler::MachineConfig cfg;
|
||
std::string cerr;
|
||
if (!cfg.load(std::string(REPO_ROOT) + "/compiler/machine.toml", &cerr)) {
|
||
return false;
|
||
}
|
||
return link_project(p, units, cfg, 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<SourceUnit> 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));
|
||
}
|
||
compiler::MachineConfig cfg;
|
||
std::string cerr;
|
||
CHECK(cfg.load(std::string(REPO_ROOT) + "/compiler/machine.toml", &cerr));
|
||
CHECK(link_project(p, units, cfg, &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_name == "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<SourceUnit> 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));
|
||
}
|
||
compiler::MachineConfig cfg;
|
||
std::string cerr;
|
||
CHECK(cfg.load(std::string(REPO_ROOT) + "/compiler/machine.toml", &cerr));
|
||
CHECK(link_project(p, units, cfg, &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_name == "bool");
|
||
CHECK(it->second.fields[1].name == "pt" && it->second.fields[1].type_name == "time");
|
||
CHECK(it->second.fields[2].name == "q" && it->second.fields[2].type_name == "bool");
|
||
CHECK(it->second.fields[3].name == "et" && it->second.fields[3].type_name == "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;
|
||
}
|