实现 12.3 toml 读取:schema 校验、文件集合、工程哈希。

- Project.h/cpp:toml++ 解析 + 字段白名单(未知键带行号报错)、name/entry/cycle_limit/dt_ms/files.st 必填、io 条目完整性、entry 必须 program MAIN
- compile_files:files.st ∪ gvl.file 去重;compute_project_hash:路径排序后 FNV-1a 64 增量,缺文件报 file missing
- STCompiler <project.toml>:打印文件列表与哈希,失败退出码 1,--help
- compiler 私有链接 toml++ v3.4.0;测试固化 compiler_toml(35 断言,line1 哈希冻结 0xc3fe4ae74ad45900)
This commit is contained in:
2026-08-21 00:32:53 +08:00
parent 6df3465b37
commit c076c25368
6 changed files with 682 additions and 5 deletions
+3 -1
View File
@@ -8,9 +8,11 @@ project(Compiler
# 词法、语法、符号表、类型检查、codegen、链接都留在 compiler 一个库里 # 词法、语法、符号表、类型检查、codegen、链接都留在 compiler 一个库里
add_library(compiler STATIC add_library(compiler STATIC
./src/Lexer.cpp) ./src/Lexer.cpp
./src/Project.cpp)
target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_include_directories(compiler PRIVATE ${CMAKE_SOURCE_DIR}/third_party/tomlplusplus/include)
target_link_libraries(compiler PUBLIC isa) target_link_libraries(compiler PUBLIC isa)
# 可执行入口:STCompiler <project.toml> -o <name>.stb # 可执行入口:STCompiler <project.toml> -o <name>.stb
+42
View File
@@ -0,0 +1,42 @@
/**
* @file Project.h
* @brief 工程定义与 project.toml 解析(schema 校验)
* @author
* @date 2026-08-21
*/
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "isa/Image.h"
namespace compiler {
// 与 Doc/初步计划.md 12.1 的 toml 字段表一一对应。
// 字段白名单 / 必填 / io 完整性在 parse_project 内校验。
struct Project {
std::string name; // [project] 必填
std::string entry; // 必填,第一版必须 "program MAIN"
uint32_t cycle_limit = 0; // 必填 > 0
uint32_t dt_ms = 0; // 必填 > 0
std::vector<std::string> files_st; // [files] 必填非空
std::string gvl_file; // [gvl] 可选;无则空串
std::vector<isa::IoBinding> io; // [[io.*]] 可选;slot 12.6 才解析,此处填 0
std::string base_dir; // toml 所在目录(解析相对路径用)
};
// 解析并校验 project.toml。成功返回 true;失败返回 false 并写 err
// err 带稳定类别前缀(unknown key / missing field / invalid value / bad entry+ 行号。
bool parse_project(const std::string& toml_path, Project* out, std::string* err);
// 编译文件集合:files.st gvl.file,去重(gvl 已在 files.st 则跳过)。
// 保持 files.st 顺序,gvl 追加在后;相对路径以 base_dir 为基准解析。
std::vector<std::string> compile_files(const Project& p);
// 校验全部文件存在并计算工程哈希:集合按路径排序,对内容做 FNV-1a 64 增量;
// 空集合 = basisisa::kFnvBasis)。缺文件报错前缀 "file missing"。
bool compute_project_hash(const Project& p, uint64_t* hash, std::string* err);
}
+366
View File
@@ -0,0 +1,366 @@
/**
* @file Project.cpp
* @brief 工程定义与 project.toml 解析(schema 校验)
* @author
* @date 2026-08-21
*/
#include "compiler/Project.h"
#include <toml++/toml.hpp>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include "isa/Image.h"
namespace compiler {
namespace {
// ---- 错误收集:稳定类别前缀 + 行号 ----
void fail(std::string* err, const std::string& msg) {
if (err) {
*err = msg;
}
}
std::string at(const toml::node& n) {
char buf[32];
std::snprintf(buf, sizeof buf, " (line %u)", n.source().begin.line);
return buf;
}
// parse_error 的位置可能是空的(如文件打不开):只在有位置时带行号
std::string at_pos(const toml::source_position& pos) {
char buf[32];
std::snprintf(buf, sizeof buf, " (line %u)", pos.line);
return static_cast<bool>(pos) ? buf : std::string();
}
// 取字符串,必填校验
bool req_string(const toml::table& tbl, const char* sec, const char* key,
std::string* out, std::string* err) {
if (const auto nv = tbl[key]) {
if (!nv.is_string()) {
fail(err, std::string("invalid value for '") + key + "' in [" + sec +
"] (expect string)" + at(*nv.node()));
return false;
}
*out = nv.value_or(std::string());
return true;
}
fail(err, std::string("missing field '") + key + "' in [" + sec + "]");
return false;
}
// 取正整数,必填校验
bool req_pos_int(const toml::table& tbl, const char* sec, const char* key,
uint32_t* out, std::string* err) {
if (const auto nv = tbl[key]) {
if (!nv.is_integer()) {
fail(err, std::string("invalid value for '") + key + "' in [" + sec +
"] (expect integer)" + at(*nv.node()));
return false;
}
const int64_t v = nv.value_or<int64_t>(0);
if (v <= 0) {
fail(err, std::string("invalid value for '") + key + "' in [" + sec +
"] (expect > 0)" + at(*nv.node()));
return false;
}
*out = static_cast<uint32_t>(v);
return true;
}
fail(err, std::string("missing field '") + key + "' in [" + sec + "]");
return false;
}
// 取非负整数,必填校验(channel / bit 允许 0
bool req_uint(const toml::table& tbl, const char* sec, const char* key,
uint32_t* out, std::string* err) {
if (const auto nv = tbl[key]) {
if (!nv.is_integer()) {
fail(err, std::string("invalid value for '") + key + "' in [" + sec +
"] (expect integer)" + at(*nv.node()));
return false;
}
const int64_t v = nv.value_or<int64_t>(0);
if (v < 0) {
fail(err, std::string("invalid value for '") + key + "' in [" + sec +
"] (expect >= 0)" + at(*nv.node()));
return false;
}
*out = static_cast<uint32_t>(v);
return true;
}
fail(err, std::string("missing field '") + key + "' in [" + sec + "]");
return false;
}
// 表内键白名单;返回 false 并报首个未知键
bool check_keys(const toml::table& tbl, const char* sec,
const char* const* allowed, size_t n_allowed,
std::string* err) {
for (const auto& [key, node] : tbl) {
bool ok = false;
for (size_t i = 0; i < n_allowed; ++i) {
if (key == allowed[i]) {
ok = true;
break;
}
}
if (!ok) {
fail(err, std::string("unknown key '") + std::string(key) + "' in [" +
sec + "]" + at(node));
return false;
}
}
return true;
}
// ---- [project] ----
bool parse_project_section(const toml::table& root, Project* out, std::string* err) {
static const char* const allowed[] = {"name", "entry", "cycle_limit", "dt_ms"};
if (const auto nv = root["project"]) {
if (!nv.is_table()) {
fail(err, "invalid value for 'project' (expect table)" + at(*nv.node()));
return false;
}
const toml::table& sec = *nv.as_table();
if (!check_keys(sec, "project", allowed, 4, err)) {
return false;
}
if (!req_string(sec, "project", "name", &out->name, err)) {
return false;
}
if (!req_string(sec, "project", "entry", &out->entry, err)) {
return false;
}
if (out->entry != "program MAIN") {
fail(err, "invalid value for 'entry' in [project] (expect \"program MAIN\")" +
at(*sec["entry"].node()));
return false;
}
if (!req_pos_int(sec, "project", "cycle_limit", &out->cycle_limit, err)) {
return false;
}
if (!req_pos_int(sec, "project", "dt_ms", &out->dt_ms, err)) {
return false;
}
return true;
}
fail(err, "missing field 'project' in <root>");
return false;
}
// ---- [files] ----
bool parse_files_section(const toml::table& root, Project* out, std::string* err) {
static const char* const allowed[] = {"st"};
if (const auto nv = root["files"]) {
if (!nv.is_table()) {
fail(err, "invalid value for 'files' (expect table)" + at(*nv.node()));
return false;
}
const toml::table& sec = *nv.as_table();
if (!check_keys(sec, "files", allowed, 1, err)) {
return false;
}
if (const auto st = sec["st"]) {
if (!st.is_array()) {
fail(err, "invalid value for 'st' in [files] (expect array)" + at(*st.node()));
return false;
}
const toml::array& arr = *st.as_array();
if (arr.empty()) {
fail(err, "invalid value for 'st' in [files] (expect non-empty array)");
return false;
}
for (const auto& el : arr) {
if (!el.is_string()) {
fail(err, "invalid value in 'st' in [files] (expect array of strings)" +
at(el));
return false;
}
out->files_st.push_back(el.value_or(std::string()));
}
return true;
}
fail(err, "missing field 'st' in [files]");
return false;
}
fail(err, "missing field 'files' in <root>");
return false;
}
// ---- [gvl](可选)----
bool parse_gvl_section(const toml::table& root, Project* out, std::string* err) {
static const char* const allowed[] = {"file"};
if (const auto nv = root["gvl"]) {
if (!nv.is_table()) {
fail(err, "invalid value for 'gvl' (expect table)" + at(*nv.node()));
return false;
}
const toml::table& sec = *nv.as_table();
if (!check_keys(sec, "gvl", allowed, 1, err)) {
return false;
}
return req_string(sec, "gvl", "file", &out->gvl_file, err);
}
return true; // gvl 可选
}
// ---- [[io.input]] / [[io.output]](可选)----
bool parse_io_entry(const toml::node& el, bool is_input, Project* out, std::string* err) {
static const char* const allowed[] = {"var", "channel", "bit"};
if (!el.is_table()) {
fail(err, std::string("bad entry in [[io.") + (is_input ? "input" : "output") +
"]] (expect table)" + at(el));
return false;
}
const toml::table& sec = *el.as_table();
if (!check_keys(sec, is_input ? "io.input" : "io.output", allowed, 3, err)) {
return false;
}
isa::IoBinding b;
b.is_input = is_input;
b.slot = 0; // 12.6 链接阶段再解析
if (!req_string(sec, is_input ? "io.input" : "io.output", "var", &b.var, err)) {
return false;
}
if (!req_uint(sec, is_input ? "io.input" : "io.output", "channel", &b.channel, err)) {
return false;
}
if (!req_uint(sec, is_input ? "io.input" : "io.output", "bit", &b.bit, err)) {
return false;
}
out->io.push_back(b);
return true;
}
bool parse_io_section(const toml::table& root, Project* out, std::string* err) {
static const char* const allowed[] = {"input", "output"};
if (const auto nv = root["io"]) {
if (!nv.is_table()) {
fail(err, "invalid value for 'io' (expect table)" + at(*nv.node()));
return false;
}
const toml::table& sec = *nv.as_table();
if (!check_keys(sec, "io", allowed, 2, err)) {
return false;
}
for (const char* which : {"input", "output"}) {
if (const auto arr = sec[which]) {
if (!arr.is_array()) {
fail(err, std::string("invalid value for '") + which +
"' in [io] (expect array of tables)" + at(*arr.node()));
return false;
}
const bool is_input = (which[0] == 'i');
for (const auto& el : *arr.as_array()) {
if (!parse_io_entry(el, is_input, out, err)) {
return false;
}
}
}
}
return true;
}
return true; // io 可选
}
// ---- 顶层 ----
bool parse_root(const toml::table& root, Project* out, std::string* err) {
static const char* const allowed[] = {"project", "files", "gvl", "io"};
if (!check_keys(root, "root", allowed, 4, err)) {
return false;
}
if (!parse_project_section(root, out, err)) {
return false;
}
if (!parse_files_section(root, out, err)) {
return false;
}
if (!parse_gvl_section(root, out, err)) {
return false;
}
return parse_io_section(root, out, err);
}
std::string dir_of(const std::string& path) {
const size_t slash = path.find_last_of("/\\");
return (slash == std::string::npos) ? "." : path.substr(0, slash);
}
} // namespace
bool parse_project(const std::string& toml_path, Project* out, std::string* err) {
out->base_dir = dir_of(toml_path);
// toml++ 默认 TOML_EXCEPTIONS=1parse_file 失败直接抛 parse_error
toml::table tbl;
try {
tbl = toml::parse_file(toml_path);
} catch (const toml::parse_error& e) {
fail(err, std::string("parse error: ") + std::string(e.description()) +
at_pos(e.source().begin));
return false;
}
return parse_root(tbl, out, err);
}
// ---- 文件集合与工程哈希 ----
std::vector<std::string> compile_files(const Project& p) {
std::vector<std::string> out = p.files_st;
if (!p.gvl_file.empty() &&
std::find(out.begin(), out.end(), p.gvl_file) == out.end()) {
out.push_back(p.gvl_file);
}
return out;
}
bool compute_project_hash(const Project& p, uint64_t* hash, std::string* err) {
std::vector<std::string> files = compile_files(p);
// 路径排序(规格:路径只当排序键)
std::vector<std::string> sorted = files;
std::sort(sorted.begin(), sorted.end());
std::filesystem::path base(p.base_dir);
uint64_t h = isa::kFnvBasis;
for (const std::string& f : sorted) {
std::ifstream in(base / f, std::ios::binary);
if (!in) {
fail(err, "file missing: " + (base / f).string());
return false;
}
char buf[4096];
while (in) {
in.read(buf, sizeof buf);
const std::streamsize n = in.gcount();
if (n > 0) {
h = isa::fnv1a64_update(h, reinterpret_cast<const uint8_t*>(buf),
static_cast<size_t>(n));
}
}
}
*hash = h;
return true;
}
} // namespace compiler
+42 -3
View File
@@ -2,14 +2,53 @@
* @file main.cpp * @file main.cpp
* @brief STCompiler 可执行入口 * @brief STCompiler 可执行入口
* @author * @author
* @date 2026-08-19 * @date 2026-08-21
*/ */
#include <cstdio> #include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include "compiler/Project.h"
namespace {
void usage() {
std::printf("usage: STCompiler <project.toml>\n"
" 解析工程并打印文件集合与工程哈希(12.3 阶段)\n"
" --help 打印本帮助\n");
}
}
int main(int argc, char** argv) { int main(int argc, char** argv) {
(void)argc; if (argc < 2) {
(void)argv;
std::printf("STCompiler 0.1\n"); std::printf("STCompiler 0.1\n");
usage();
return 0;
}
if (std::strcmp(argv[1], "--help") == 0) {
usage();
return 0;
}
compiler::Project proj;
std::string err;
if (!compiler::parse_project(argv[1], &proj, &err)) {
std::fprintf(stderr, "error: %s\n", err.c_str());
return 1;
}
const std::vector<std::string> files = compiler::compile_files(proj);
uint64_t hash = 0;
if (!compiler::compute_project_hash(proj, &hash, &err)) {
std::fprintf(stderr, "error: %s\n", err.c_str());
return 1;
}
std::printf("project: %s\n", proj.name.c_str());
std::printf("files:\n");
for (const std::string& f : files) {
std::printf(" %s\n", f.c_str());
}
std::printf("hash: 0x%016llx\n", static_cast<unsigned long long>(hash));
return 0; return 0;
} }
+11
View File
@@ -12,3 +12,14 @@ target_link_libraries(isa_test PRIVATE isa)
add_test(NAME isa_roundtrip add_test(NAME isa_roundtrip
COMMAND isa_test) COMMAND isa_test)
# compiler 库测试:toml schema、文件集合、工程哈希(REPO_ROOT 注入源目录绝对路径)
add_executable(compiler_test
./src/compiler_test.cpp)
target_link_libraries(compiler_test PRIVATE compiler)
target_compile_definitions(compiler_test PRIVATE
REPO_ROOT="${CMAKE_SOURCE_DIR}")
add_test(NAME compiler_toml
COMMAND compiler_test)
+217
View File
@@ -0,0 +1,217 @@
/**
* @file compiler_test.cpp
* @brief compiler 库测试:toml schema、文件集合、工程哈希
* @author
* @date 2026-08-21
*/
#include <cstdio>
#include <filesystem>
#include <string>
#include <vector>
#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)
// ---- 写临时 toml(负例用),返回路径 ----
static std::string write_tmp(const char* name, const char* content) {
const std::filesystem::path dir = std::filesystem::temp_directory_path();
const std::filesystem::path path = dir / name;
std::FILE* f = std::fopen(path.string().c_str(), "w");
std::fputs(content, f);
std::fclose(f);
return path.string();
}
static void rm_tmp(const std::string& path) {
std::remove(path.c_str());
}
// ---- 1. line1 正例 ----
static bool test_line1() {
using namespace compiler;
Project p;
std::string err;
CHECK(parse_project(std::string(REPO_ROOT) + "/examples/line1/project.toml", &p, &err));
CHECK(p.name == "line1");
CHECK(p.entry == "program MAIN");
CHECK(p.cycle_limit == 100000);
CHECK(p.dt_ms == 10);
CHECK(p.files_st.size() == 3);
CHECK(p.files_st[0] == "globals.st");
CHECK(p.gvl_file == "globals.st");
CHECK(p.io.size() == 3);
CHECK(p.io[0].var == "EmergencyStop" && p.io[0].is_input && p.io[0].bit == 2);
CHECK(p.io[2].var == "Q0_0" && !p.io[2].is_input);
CHECK(p.base_dir == std::string(REPO_ROOT) + "/examples/line1");
const std::vector<std::string> files = compile_files(p);
CHECK(files.size() == 3); // gvl 已在 files.st,不重复
uint64_t h = 0;
CHECK(compute_project_hash(p, &h, &err));
CHECK(h == 0xc3fe4ae74ad45900ull); // 冻结值:确定性回放
return true;
}
// ---- 2. 无 io / 无 gvl 的用例 ----
static bool test_minimal() {
using namespace compiler;
Project p;
std::string err;
CHECK(parse_project(std::string(REPO_ROOT) + "/tests/cases/01_empty_main/project.toml", &p, &err));
CHECK(p.gvl_file.empty());
CHECK(p.io.empty());
CHECK(compile_files(p).size() == 1);
CHECK(parse_project(std::string(REPO_ROOT) + "/tests/cases/15_fb_instance/project.toml", &p, &err));
CHECK(p.gvl_file.empty());
return true;
}
// ---- 3. gvl.file 不在 files.st 时追加 ----
static bool test_gvl_append() {
using namespace compiler;
const std::string path = write_tmp(
"stator_gvl_append.toml",
"[project]\nname = \"x\"\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n"
"[files]\nst = [\"main.st\"]\n[gvl]\nfile = \"globals.st\"\n");
Project p;
std::string err;
CHECK(parse_project(path, &p, &err));
const std::vector<std::string> files = compile_files(p);
CHECK(files.size() == 2);
CHECK(files[0] == "main.st");
CHECK(files[1] == "globals.st");
rm_tmp(path);
return true;
}
// ---- 4. 负例:schema 校验类别 ----
static bool expect_err(const std::string& path, const char* prefix) {
compiler::Project p;
std::string err;
if (compiler::parse_project(path, &p, &err)) {
std::printf("FAIL %s: parsed ok\n", path.c_str());
return false;
}
if (err.find(prefix) != 0) {
std::printf("FAIL %s: want '%s', got '%s'\n", path.c_str(), prefix, err.c_str());
return false;
}
++g_checks;
return true;
}
static bool test_negative() {
using namespace compiler;
const std::string b1 = write_tmp(
"stator_bad1.toml",
"[project]\nname = \"x\"\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n"
"[files]\nst = [\"a.st\"]\n[bogus]\nx = 1\n");
if (!expect_err(b1, "unknown key")) return false;
const std::string b2 = write_tmp(
"stator_bad2.toml",
"[project]\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n"
"[files]\nst = [\"a.st\"]\n");
if (!expect_err(b2, "missing field")) return false;
const std::string b3 = write_tmp(
"stator_bad3.toml",
"[project]\nname = \"x\"\nentry = \"program foo\"\ncycle_limit = 1\ndt_ms = 1\n"
"[files]\nst = [\"a.st\"]\n");
if (!expect_err(b3, "invalid value")) return false;
const std::string b4 = write_tmp(
"stator_bad4.toml",
"[project]\nname = \"x\"\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n"
"[files]\nst = [\"a.st\"]\n[[io.input]]\nvar = \"I0_0\"\nchannel = 0\n");
if (!expect_err(b4, "missing field")) return false;
const std::string b5 = write_tmp("stator_bad5.toml", "[project\nname = \"x\"\n");
if (!expect_err(b5, "parse error")) return false;
// 缺文件(compute_project_hash 层)
const std::string b6 = write_tmp(
"stator_bad6.toml",
"[project]\nname = \"x\"\nentry = \"program MAIN\"\ncycle_limit = 1\ndt_ms = 1\n"
"[files]\nst = [\"nope.st\"]\n");
Project p;
std::string err;
CHECK(parse_project(b6, &p, &err));
uint64_t h = 0;
CHECK(!compute_project_hash(p, &h, &err));
CHECK(err.find("file missing") == 0);
// toml 文件本身不存在
if (!expect_err(std::filesystem::temp_directory_path().string() + "/stator_nope.toml",
"parse error")) {
return false;
}
rm_tmp(b1);
rm_tmp(b2);
rm_tmp(b3);
rm_tmp(b4);
rm_tmp(b5);
rm_tmp(b6);
return true;
}
// ---- 5. 20 个用例全部可解析 + 可哈希 ----
static bool test_all_cases() {
using namespace compiler;
int n = 0;
for (const auto& dir : std::filesystem::directory_iterator(
std::string(REPO_ROOT) + "/tests/cases")) {
if (!dir.is_directory()) {
continue;
}
const std::string toml = dir.path().string() + "/project.toml";
Project p;
std::string err;
if (!parse_project(toml, &p, &err)) {
std::printf("FAIL parse %s: %s\n", toml.c_str(), err.c_str());
return false;
}
uint64_t h = 0;
if (!compute_project_hash(p, &h, &err)) {
std::printf("FAIL hash %s: %s\n", toml.c_str(), err.c_str());
return false;
}
++n;
}
CHECK(n == 20);
return true;
}
int main() {
if (!test_line1()) return 1;
if (!test_minimal()) return 1;
if (!test_gvl_append()) return 1;
if (!test_negative()) return 1;
if (!test_all_cases()) return 1;
std::printf("compiler_test: %d checks passed\n", g_checks);
return 0;
}