fix: review fixes + feat: imports, assertions, merge engine, per-package overrides

Review fixes (8 blocking issues):
- Extract ParseError to shared error.hpp (ODR fix)
- Remove dead package.cpp/package.hpp + tomlplusplus dep
- Safe parse_int() helper replacing crash-prone std::stoi
- consume_string() now accepts bare numbers and idents
- line_at() fixed for post-EOF line numbers
- Subcommand validation before file read in CLI
- KwService/KwAssert/KwImport added to consume_ident()
- root partition promoted to first-class BootBlock field

New features:
- Imports: imports = [...] with recursive merge resolution
- Assertions: assert { "msg" : field op value } in both parsers
- Merge engine: resolve_package() with features/config merge + force support
- Per-package overrides: /kappa/system/builds/<name>.kap
- .gitignore: added vcpkg_installed/ and kappa binary
This commit is contained in:
2026-07-29 23:52:03 -04:00
parent d5f7d397ed
commit 0c1e82d66b
18 changed files with 325 additions and 212 deletions
+42 -9
View File
@@ -1,4 +1,5 @@
#include "kappa/dsl/parser.hpp"
#include "kappa/dsl/error.hpp"
#include "kappa/dsl/lexer.hpp"
#include <format>
@@ -8,12 +9,6 @@
namespace kappa::dsl {
class ParseError : public std::runtime_error {
public:
ParseError(int line, int col, const std::string& msg)
: std::runtime_error(std::format("{}:{}: {}", line, col, msg)) {}
};
class Parser {
public:
explicit Parser(std::string_view source) : lexer_(source) { advance(); }
@@ -215,7 +210,7 @@ void Parser::parse_body(PackageDef& pkg) {
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto init_name = current_.lexeme; // e.g. "runit", "s6"
auto init_name = current_.lexeme;
advance();
consume(TokenType::Lbrace);
skip_newlines();
@@ -236,7 +231,8 @@ void Parser::parse_body(PackageDef& pkg) {
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
si.ports.push_back(
std::stoi(std::string(current_.lexeme)));
parse_int(current_.line, current_.col,
current_.lexeme));
advance();
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
@@ -255,6 +251,42 @@ void Parser::parse_body(PackageDef& pkg) {
consume(TokenType::Rbrace);
break;
case TokenType::KwAssert:
consume(TokenType::KwAssert);
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
Assertion a;
a.message = consume(TokenType::String).lexeme;
consume(TokenType::Ident); // ":"
a.field = current_.lexeme;
advance();
if (at(TokenType::Equals)) {
advance();
if (at(TokenType::Equals)) {
a.op = "=="; advance();
} else {
a.op = "=";
}
} else {
a.op = current_.lexeme; advance();
}
if (at(TokenType::String)) {
a.value = consume(TokenType::String).lexeme;
} else if (at(TokenType::KwTrue)) {
a.value = "true"; advance();
} else if (at(TokenType::KwFalse)) {
a.value = "false"; advance();
} else {
a.value = current_.lexeme; advance();
}
pkg.assertions.push_back(std::move(a));
skip_newlines();
}
consume(TokenType::Rbrace);
break;
case TokenType::KwPrepare:
consume(TokenType::KwPrepare);
pkg.prepare = parse_phase();
@@ -420,7 +452,8 @@ Patch Parser::parse_patch_item() {
} else if (key == "sha256") {
p.sha256 = consume(TokenType::String).lexeme;
} else if (key == "level") {
p.level = std::stoi(std::string(current_.lexeme));
p.level = parse_int(current_.line, current_.col,
current_.lexeme);
advance();
}
skip_newlines();