feat: DSL parser, eval system, and project structure
- Hand-written lexer + recursive-descent parser for .kap package definitions
- Package constructs: const, license, provides, outputs, patches, depends
(version-constrained, feature-gated, output-targeted), features (enabled/
forced/flag), config files (default/replace/merge with cfg interpolation),
env (?= soft-set), prepare/build/check/install phases
- Evaluator: recursive resolver for prefix, jobopts, destdir, userargs,
cfg.*, feature.*, package vars
- Comments: // single-line, /* */ multi-line
- /kappa/{bin,temp,db,system,system/builds} path layout
- Clang 22, C++23, CMake + vcpkg (tomlplusplus)
- .clangd + .clang-tidy configured
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
#include "kappa/dsl/lexer.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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(); }
|
||||
|
||||
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::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 = std::stoi(std::string(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
|
||||
Reference in New Issue
Block a user