#include "kappa/dsl/parser.hpp" #include "kappa/dsl/error.hpp" #include "kappa/dsl/lexer.hpp" #include #include #include #include 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 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 Parser::parse_string_list() { consume(TokenType::Lbracket); skip_newlines(); std::vector 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