feat: system config DSL, services, CLI with rustc diagnostics

- System config parser (system/packages/services/boot/users blocks)
- Boot block: kernel, init, efi, swap, bootloader, root partition
- System-wide features, rollback config, swap partition
- Service block: per-init variants (runit, s6, etc.), system init selection via boot.init
- Feature-gated dependencies, config values accept numbers/bools/strings
- CLI: parse-package, parse-config, validate subcommands
- Rustc-style error diagnostics with source context, carets, help text
- Diagnostic module with ANSI color output
This commit is contained in:
2026-07-29 22:50:39 -04:00
parent a8b53681fc
commit d5f7d397ed
12 changed files with 850 additions and 19 deletions
+358
View File
@@ -0,0 +1,358 @@
#include "kappa/dsl/system.hpp"
#include "kappa/dsl/lexer.hpp"
#include <format>
#include <stdexcept>
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 SysParser {
public:
explicit SysParser(std::string_view source) : lexer_(source) { advance(); }
SystemConfig parse();
private:
void advance();
Token consume(TokenType type);
void skip_newlines();
bool at(TokenType type) const { return current_.type == type; }
bool parse_bool();
void parse_system_block(SystemConfig& cfg);
void parse_packages_block(SystemConfig& cfg);
void parse_services_block(SystemConfig& cfg);
void parse_boot_block(SystemConfig& cfg);
void parse_users_block(SystemConfig& cfg);
std::string consume_ident();
std::string consume_string();
Token current_;
Lexer lexer_;
};
void SysParser::advance() { current_ = lexer_.next(); }
void SysParser::skip_newlines() {
while (at(TokenType::Newline)) { advance(); }
}
Token SysParser::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;
}
std::string SysParser::consume_ident() {
if (at(TokenType::Ident) ||
at(TokenType::KwEnv) ||
at(TokenType::KwConfig) ||
at(TokenType::KwFeatures) ||
at(TokenType::KwVersion) ||
at(TokenType::KwSource) ||
at(TokenType::KwLicense) ||
at(TokenType::KwPatches) ||
at(TokenType::KwConst) ||
at(TokenType::KwPackage) ||
at(TokenType::KwDepends) ||
at(TokenType::KwProvides) ||
at(TokenType::KwOutputs) ||
at(TokenType::KwPrepare) ||
at(TokenType::KwBuild) ||
at(TokenType::KwCheck) ||
at(TokenType::KwInstall) ||
at(TokenType::KwTrue) ||
at(TokenType::KwFalse)) {
auto lexeme = current_.lexeme;
advance();
return lexeme;
}
throw ParseError(current_.line, current_.col,
std::format("expected identifier, got '{}'",
token_name(current_.type)));
}
std::string SysParser::consume_string() {
if (at(TokenType::KwTrue)) { advance(); return "true"; }
if (at(TokenType::KwFalse)) { advance(); return "false"; }
return consume(TokenType::String).lexeme;
}
bool SysParser::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)));
}
SystemConfig SysParser::parse() {
skip_newlines();
SystemConfig cfg;
while (!at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Eof)) { break; }
auto kw = consume_ident();
if (kw == "system") { parse_system_block(cfg); }
else if (kw == "packages") { parse_packages_block(cfg); }
else if (kw == "services") { parse_services_block(cfg); }
else if (kw == "boot") { parse_boot_block(cfg); }
else if (kw == "users") { parse_users_block(cfg); }
else {
throw ParseError(current_.line, current_.col,
std::format("unknown section '{}'", kw));
}
}
return cfg;
}
void SysParser::parse_system_block(SystemConfig& cfg) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Rbrace)) { break; }
auto key = consume_ident();
if (key == "hostname") {
consume(TokenType::Equals);
cfg.system.hostname = consume_string();
} else if (key == "timezone") {
consume(TokenType::Equals);
cfg.system.timezone = consume_string();
} else if (key == "env") {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto k = consume_ident();
bool soft = false;
if (at(TokenType::Ident) && current_.lexeme == "?=") {
soft = true;
advance();
} else {
consume(TokenType::Equals);
}
cfg.system.env.push_back({std::move(k), consume_string(), soft});
skip_newlines();
}
consume(TokenType::Rbrace);
} else if (key == "config") {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto k = consume_ident();
consume(TokenType::Equals);
cfg.system.config[k] = consume_string();
skip_newlines();
}
consume(TokenType::Rbrace);
} else if (key == "features") {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto k = consume_ident();
consume(TokenType::Equals);
cfg.system.features[k] = FeatureDef{parse_bool(), false, ""};
skip_newlines();
}
consume(TokenType::Rbrace);
} else if (key == "rollback") {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto k = consume_ident();
consume(TokenType::Equals);
if (k == "keep") {
cfg.system.rollback.keep = std::stoi(std::string(current_.lexeme));
advance();
}
skip_newlines();
}
consume(TokenType::Rbrace);
} else {
throw ParseError(current_.line, current_.col,
std::format("unknown system key '{}'", key));
}
}
consume(TokenType::Rbrace);
}
void SysParser::parse_packages_block(SystemConfig& cfg) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Rbrace)) { break; }
PackageRef pkg;
pkg.name = consume_ident();
if (at(TokenType::Lbrace)) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Rbrace)) { break; }
auto key = consume_ident();
if (key == "version") {
consume(TokenType::Equals);
pkg.version = consume_string();
} else if (key == "features") {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto k = consume_ident();
consume(TokenType::Equals);
pkg.features[k] = FeatureDef{parse_bool(), false, ""};
skip_newlines();
}
consume(TokenType::Rbrace);
} else if (key == "config") {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto k = consume_ident();
consume(TokenType::Equals);
pkg.config[k] = current_.lexeme;
advance();
skip_newlines();
}
consume(TokenType::Rbrace);
}
}
consume(TokenType::Rbrace);
}
cfg.packages.push_back(std::move(pkg));
}
consume(TokenType::Rbrace);
}
void SysParser::parse_services_block(SystemConfig& cfg) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Rbrace)) { break; }
ServiceRef svc;
svc.name = consume_ident();
if (at(TokenType::Lbrace)) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = consume_ident();
if (key == "enable") {
consume(TokenType::Equals);
svc.enable = parse_bool();
} else {
consume(TokenType::Equals);
svc.config[key] = current_.lexeme;
advance();
}
skip_newlines();
}
consume(TokenType::Rbrace);
}
cfg.services.push_back(std::move(svc));
}
consume(TokenType::Rbrace);
}
void SysParser::parse_boot_block(SystemConfig& cfg) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Rbrace)) { break; }
auto key = consume_ident();
consume(TokenType::Equals);
auto val = consume_string();
if (key == "kernel") { cfg.boot.kernel = std::move(val); }
else if (key == "init") { cfg.boot.init = std::move(val); }
else if (key == "efi") { cfg.boot.efi = std::move(val); }
else if (key == "swap") { cfg.boot.swap = std::move(val); }
else if (key == "bootloader"){ cfg.boot.bootloader = std::move(val); }
else { cfg.boot.params[key] = std::move(val); }
skip_newlines();
}
consume(TokenType::Rbrace);
}
void SysParser::parse_users_block(SystemConfig& cfg) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Rbrace)) { break; }
UserRef u;
u.name = consume_ident();
if (at(TokenType::Lbrace)) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = consume_ident();
if (key == "shell") {
consume(TokenType::Equals);
u.shell = consume_string();
} else if (key == "groups") {
consume(TokenType::Equals);
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
u.groups.push_back(consume_string());
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
} else {
consume(TokenType::Equals);
u.extra[key] = consume_string();
}
skip_newlines();
}
consume(TokenType::Rbrace);
}
cfg.users.push_back(std::move(u));
}
consume(TokenType::Rbrace);
}
SystemConfig parse_system_config(std::string_view source) {
SysParser p(source);
return p.parse();
}
} // namespace kappa::dsl