Files
kappa/src/dsl/parser.cpp
T
huntedbytheirs 0c1e82d66b 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
2026-07-29 23:52:03 -04:00

496 lines
17 KiB
C++

#include "kappa/dsl/parser.hpp"
#include "kappa/dsl/error.hpp"
#include "kappa/dsl/lexer.hpp"
#include <format>
#include <stdexcept>
#include <string>
#include <vector>
namespace kappa::dsl {
class Parser {
public:
explicit Parser(std::string_view source) : lexer_(source) { advance(); }
PackageDef parse();
private:
void advance();
Token consume(TokenType type);
void skip_newlines();
bool at(TokenType type) const { return current_.type == type; }
PackageDef parse_package_def();
void parse_body(PackageDef& pkg);
Phase parse_phase();
Command parse_command_line();
bool parse_bool();
std::vector<std::string> parse_string_list();
Dependency parse_dependency_item();
ConfigFile parse_config_file();
FeatureDef parse_feature_def();
Patch parse_patch_item();
Token current_;
Lexer lexer_;
};
void Parser::advance() { current_ = lexer_.next(); }
void Parser::skip_newlines() {
while (at(TokenType::Newline)) { advance(); }
}
Token Parser::consume(TokenType type) {
if (!at(type)) {
throw ParseError(current_.line, current_.col,
std::format("expected '{}', got '{}'",
token_name(type), token_name(current_.type)));
}
Token t = std::move(current_);
advance();
return t;
}
PackageDef Parser::parse() {
skip_newlines();
return parse_package_def();
}
PackageDef Parser::parse_package_def() {
consume(TokenType::KwPackage);
auto name_tok = consume(TokenType::String);
consume(TokenType::Lbrace);
PackageDef pkg;
pkg.name = std::move(name_tok.lexeme);
parse_body(pkg);
consume(TokenType::Rbrace);
return pkg;
}
void Parser::parse_body(PackageDef& pkg) {
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Rbrace) || at(TokenType::Eof)) { break; }
switch (current_.type) {
case TokenType::KwConst: {
consume(TokenType::KwConst);
if (at(TokenType::KwVersion)) {
consume(TokenType::KwVersion);
consume(TokenType::Equals);
pkg.version = consume(TokenType::String).lexeme;
pkg.const_keys.insert("version");
} else if (at(TokenType::KwSource)) {
consume(TokenType::KwSource);
consume(TokenType::Equals);
pkg.source = consume(TokenType::String).lexeme;
pkg.const_keys.insert("source");
} else {
throw ParseError(current_.line, current_.col,
std::format("unexpected token '{}' after const",
token_name(current_.type)));
}
break;
}
case TokenType::KwVersion:
consume(TokenType::KwVersion);
consume(TokenType::Equals);
pkg.version = consume(TokenType::String).lexeme;
break;
case TokenType::KwSource:
consume(TokenType::KwSource);
consume(TokenType::Equals);
pkg.source = consume(TokenType::String).lexeme;
break;
case TokenType::KwLicense:
consume(TokenType::KwLicense);
consume(TokenType::Equals);
pkg.license = consume(TokenType::String).lexeme;
break;
case TokenType::KwProvides:
consume(TokenType::KwProvides);
consume(TokenType::Equals);
pkg.provides = parse_string_list();
break;
case TokenType::KwOutputs:
consume(TokenType::KwOutputs);
consume(TokenType::Equals);
pkg.outputs = parse_string_list();
break;
case TokenType::KwDepends:
consume(TokenType::KwDepends);
consume(TokenType::Equals);
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
pkg.depends.push_back(parse_dependency_item());
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
break;
case TokenType::KwFeatures:
consume(TokenType::KwFeatures);
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = consume(TokenType::Ident).lexeme;
consume(TokenType::Equals);
pkg.features[key] = parse_feature_def();
skip_newlines();
}
consume(TokenType::Rbrace);
break;
case TokenType::KwConfig:
consume(TokenType::KwConfig);
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
pkg.config_files.push_back(parse_config_file());
skip_newlines();
}
consume(TokenType::Rbrace);
break;
case TokenType::KwPatches:
consume(TokenType::KwPatches);
consume(TokenType::Equals);
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
pkg.patches.push_back(parse_patch_item());
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
break;
case TokenType::KwEnv:
consume(TokenType::KwEnv);
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = consume(TokenType::Ident).lexeme;
bool soft = false;
if (at(TokenType::Ident) && current_.lexeme == "?=") {
soft = true;
advance();
} else {
consume(TokenType::Equals);
}
pkg.env_entries.push_back(
{std::move(key), consume(TokenType::String).lexeme, soft});
skip_newlines();
}
consume(TokenType::Rbrace);
break;
case TokenType::KwService:
consume(TokenType::KwService);
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto init_name = current_.lexeme;
advance();
consume(TokenType::Lbrace);
skip_newlines();
ServiceInit si;
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = current_.lexeme;
advance();
consume(TokenType::Equals);
if (key == "exec") {
si.exec = consume(TokenType::String).lexeme;
} else if (key == "type") {
si.type = consume(TokenType::String).lexeme;
} else if (key == "user") {
si.user = consume(TokenType::String).lexeme;
} else if (key == "ports") {
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
si.ports.push_back(
parse_int(current_.line, current_.col,
current_.lexeme));
advance();
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
} else {
si.env[key] = consume(TokenType::String).lexeme;
}
skip_newlines();
}
consume(TokenType::Rbrace);
pkg.service[std::string(init_name)] = std::move(si);
skip_newlines();
}
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();
break;
case TokenType::KwBuild:
consume(TokenType::KwBuild);
pkg.build = parse_phase();
break;
case TokenType::KwCheck:
consume(TokenType::KwCheck);
pkg.check = parse_phase();
break;
case TokenType::KwInstall:
consume(TokenType::KwInstall);
pkg.install = parse_phase();
break;
default:
throw ParseError(current_.line, current_.col,
std::format("unexpected token '{}' in package body",
token_name(current_.type)));
}
}
}
std::vector<std::string> Parser::parse_string_list() {
consume(TokenType::Lbracket);
skip_newlines();
std::vector<std::string> items;
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
items.push_back(consume(TokenType::String).lexeme);
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
return items;
}
Dependency Parser::parse_dependency_item() {
if (at(TokenType::String)) {
auto raw = consume(TokenType::String).lexeme;
auto colon = raw.find(':');
if (colon != std::string::npos) {
return {raw.substr(0, colon), "", raw.substr(colon + 1)};
}
return {std::move(raw), "", ""};
}
consume(TokenType::Lbrace);
skip_newlines();
Dependency dep;
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = current_.lexeme; // accept Ident or keyword as object key
advance();
consume(TokenType::Equals);
if (key == "name") { dep.name = consume(TokenType::String).lexeme; }
else if (key == "version") { dep.version = consume(TokenType::String).lexeme; }
else if (key == "output") { dep.output = consume(TokenType::String).lexeme; }
else if (key == "feature") { dep.feature = consume(TokenType::String).lexeme; }
else { throw ParseError(current_.line, current_.col,
std::format("unknown dependency key '{}'", key)); }
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbrace);
return dep;
}
ConfigFile Parser::parse_config_file() {
consume(TokenType::Ident); // "file"
ConfigFile cf;
cf.path = consume(TokenType::String).lexeme;
if (at(TokenType::Ident) && current_.lexeme == "mode") {
advance();
consume(TokenType::Equals);
cf.mode = consume(TokenType::String).lexeme;
}
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = current_.lexeme;
advance();
consume(TokenType::Equals);
std::string val;
while (!at(TokenType::Newline) && !at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (!val.empty() && !at(TokenType::Comma)) { val += ' '; }
val += current_.lexeme;
advance();
}
cf.entries[key] = std::move(val);
skip_newlines();
}
consume(TokenType::Rbrace);
return cf;
}
bool Parser::parse_bool() {
if (at(TokenType::KwTrue)) { advance(); return true; }
if (at(TokenType::KwFalse)) { advance(); return false; }
throw ParseError(current_.line, current_.col,
std::format("expected 'true' or 'false', got '{}'",
token_name(current_.type)));
}
FeatureDef Parser::parse_feature_def() {
if (at(TokenType::KwTrue) || at(TokenType::KwFalse)) {
return {parse_bool(), false, ""};
}
consume(TokenType::Lbrace);
skip_newlines();
FeatureDef f;
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = current_.lexeme;
advance();
consume(TokenType::Equals);
if (key == "enabled") {
f.enabled = parse_bool();
} else if (key == "force") {
f.force = parse_bool();
} else if (key == "flag") {
std::string val;
while (!at(TokenType::Newline) && !at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (!val.empty() && !at(TokenType::Comma)) { val += ' '; }
val += current_.lexeme;
advance();
}
f.flag = std::move(val);
}
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbrace);
return f;
}
Patch Parser::parse_patch_item() {
if (at(TokenType::String)) {
return {consume(TokenType::String).lexeme, "", 1};
}
consume(TokenType::Lbrace);
skip_newlines();
Patch p;
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = current_.lexeme;
advance();
consume(TokenType::Equals);
if (key == "url") {
p.url = consume(TokenType::String).lexeme;
} else if (key == "sha256") {
p.sha256 = consume(TokenType::String).lexeme;
} else if (key == "level") {
p.level = parse_int(current_.line, current_.col,
current_.lexeme);
advance();
}
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbrace);
return p;
}
Phase Parser::parse_phase() {
consume(TokenType::Lbrace);
skip_newlines();
Phase phase;
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
phase.commands.push_back(parse_command_line());
}
consume(TokenType::Rbrace);
return phase;
}
Command Parser::parse_command_line() {
std::string cmd;
while (!at(TokenType::Newline) && !at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (!cmd.empty() && !at(TokenType::Comma)) { cmd += ' '; }
cmd += current_.lexeme;
advance();
}
return cmd;
}
PackageDef parse(std::string_view source) {
Parser p(source);
return p.parse();
}
} // namespace kappa::dsl