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
496 lines
17 KiB
C++
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
|