Compare commits

...
3 Commits
Author SHA1 Message Date
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
huntedbytheirs d5f7d397ed 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
2026-07-29 22:50:39 -04:00
huntedbytheirs a8b53681fc 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
2026-07-29 22:16:16 -04:00
27 changed files with 2226 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
Checks: >
-*,
bugprone-*,
modernize-*,
performance-*,
portability-*,
readability-*,
-modernize-use-trailing-return-type,
-readability-identifier-length,
-readability-magic-numbers,
-readability-identifier-naming
CheckOptions:
- key: modernize-use-nullptr.NullptrMacros
value: 'NULL'
+5
View File
@@ -0,0 +1,5 @@
CompileFlags:
CompilationDatabase: build
Diagnostics:
UnusedIncludes: Strict
MissingIncludes: Strict
+6
View File
@@ -32,3 +32,9 @@
*.out
*.app
# Build
build/
compile_commands.json
vcpkg_installed/
kappa
+32
View File
@@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.20)
# Enforce Clang
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
project(kappa VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Generate compile_commands.json for clangd
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- vcpkg integration ---
if(DEFINED ENV{VCPKG_ROOT})
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
CACHE STRING "vcpkg toolchain")
endif()
add_executable(kappa
src/main.cpp
src/paths.cpp
src/dsl/lexer.cpp
src/dsl/parser.cpp
src/dsl/system.cpp
src/eval/vars.cpp
src/cli/diagnostic.cpp
src/config/merge.cpp
)
target_include_directories(kappa PRIVATE include)
+17
View File
@@ -0,0 +1,17 @@
/*
* Per-package override for foo.
* Lives at /kappa/system/builds/foo.kap
*
* Uses system config syntax — only features/config need to be specified.
* The rest inherits from the package definition and system config.
*/
packages {
foo {
features {
debug = true
}
config {
port = "9090"
}
}
}
+95
View File
@@ -0,0 +1,95 @@
/*
* Kappa system configuration.
* Lives at /kappa/system/config.kap
*/
imports = []
assert {
"efi partition required for UEFI boot" : boot.efi != ""
"root partition must be set" : boot.root != ""
}
system {
hostname = "kappa.local"
timezone = "America/New_York"
features {
ssl = true
wayland = false
pulseaudio = false
}
env {
CFLAGS = "-O2 -march=native"
LDFLAGS = "-Wl,--as-needed"
MAKEFLAGS = "-j8"
}
config {
prefix = "/usr"
log_dir = "/var/log"
}
rollback {
keep = 5
}
}
packages {
foo {
version = ">=1.2"
features {
gui = true
}
config {
port = "9090"
ssl = true
}
}
bar {}
baz {
features {
ssl = false
}
}
/*
// Per-package overrides can also live in
// /kappa/system/builds/<name>.kap
*/
}
services {
nginx {
enable = true
port = 80
ssl = true
}
sshd {
enable = true
port = 22
}
cron {
enable = false
}
}
boot {
kernel = "linux"
init = "s6"
efi = "/dev/sda2"
swap = "/dev/sda3"
bootloader = "limine"
root = "/dev/sda1"
}
users {
specter {
shell = "/bin/zsh"
groups = ["wheel", "audio", "docker"]
}
root {
shell = "/bin/zsh"
}
}
+92
View File
@@ -0,0 +1,92 @@
/*
* foo — a web server with optional SSL and GUI support.
* Demonstrates the full kappa DSL surface.
*/
package "foo" {
const version = "1.2.3"
const source = "https://example.com/foo-${version}.tar.gz"
license = "MIT"
provides = ["libfoo.so.1", "foo"]
patches = [
{
url = "https://example.com/fix-build.patch"
sha256 = "abc123def456"
level = 1
},
"local-fix.patch" // local shorthand, no hash
]
outputs = ["bin", "lib", "dev"]
depends = [
{ name = "zlib", version = ">=1.2,<2.0" },
{ name = "openssl", feature = "ssl" },
{ name = "gtk", feature = "gui", version = ">=3" },
"gettext:lib" // name:output shorthand
]
features {
ssl = { enabled = true, flag = "--with-ssl-dir=${cfg.ssl_dir}" }
gui = { enabled = false, flag = "--enable-gui" }
drivers = { enabled = true, force = true }
debug = false
}
config {
// Written once, left alone on rebuild if the user edits it.
file "etc/foo.conf" mode = "default" {
hostname = ${cfg.hostname !} // required — user must set
listen_port = ${cfg.port ? 8080} // optional, default 8080
ssl_enabled = ${cfg.ssl ? true} // optional, default true
}
// Always overwritten on rebuild.
file "etc/log.conf" mode = "replace" {
log_level = ${cfg.log_level ? info}
log_path = ${cfg.log_path ? /var/log/foo}
}
// Three-way diff on rebuild.
file "etc/limits.conf" mode = "merge" {
max_connections = ${cfg.max_conn ? 1024}
}
}
env {
CFLAGS = "-O2 -march=native"
LDFLAGS = "-Wl,--as-needed"
CFLAGS ?= "-g"
}
service {
runit {
exec = "/usr/bin/foo --daemon"
type = "forking"
user = "foo"
}
s6 {
exec = "/usr/bin/foo"
type = "longrun"
ports = [80, 443]
user = "foo"
}
}
prepare {
patch "fix-build.patch"
}
build {
./configure --prefix=${prefix} ${feature.ssl} ${feature.gui}
make -j${jobs}
}
check {
make check
}
install {
make DESTDIR=${destdir} install
}
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <ostream>
#include <string>
#include <string_view>
namespace kappa::cli {
struct SourceLocation {
std::string_view file;
int line = 0;
int col = 0;
};
void print_error(std::ostream& os,
std::string_view source,
SourceLocation loc,
std::string_view message);
void print_error(std::ostream& os,
std::string_view source,
SourceLocation loc,
std::string_view message,
std::string_view help);
void print_note(std::ostream& os,
std::string_view source,
SourceLocation loc,
std::string_view message);
} // namespace kappa::cli
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include "kappa/dsl/system.hpp"
#include <optional>
#include <unordered_map>
#include <string>
namespace kappa::config {
struct ResolvedPackage {
dsl::PackageDef original;
std::unordered_map<std::string, dsl::FeatureDef> features;
std::unordered_map<std::string, std::string> config;
};
ResolvedPackage resolve_package(
const dsl::PackageDef& pkg,
const dsl::SystemBlock& system,
const std::optional<dsl::PackageRef>& overrides);
std::unordered_map<std::string, std::string> resolve_config(
const dsl::PackageDef& pkg,
const dsl::SystemBlock& system,
const std::optional<dsl::PackageRef>& overrides);
} // namespace kappa::config
+83
View File
@@ -0,0 +1,83 @@
#pragma once
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace kappa::dsl {
using Command = std::string;
struct Phase {
std::vector<Command> commands;
};
struct Dependency {
std::string name;
std::string version;
std::string output;
std::string feature; // empty = unconditional
};
struct ConfigFile {
std::string path;
std::string mode;
std::unordered_map<std::string, std::string> entries;
};
struct FeatureDef {
bool enabled = false;
bool force = false;
std::string flag;
};
struct Patch {
std::string url;
std::string sha256;
int level = 1;
};
struct EnvEntry {
std::string key;
std::string value;
bool soft = false;
};
struct ServiceInit {
std::string exec;
std::string type;
std::string user;
std::vector<int> ports;
std::unordered_map<std::string, std::string> env;
};
struct Assertion {
std::string message;
std::string field;
std::string op; // "==" or "!="
std::string value;
};
struct PackageDef {
std::string name;
std::string version;
std::string source;
std::string license;
std::vector<Dependency> depends;
std::vector<std::string> provides;
std::vector<std::string> outputs;
std::unordered_map<std::string, FeatureDef> features;
std::vector<ConfigFile> config_files;
std::vector<Patch> patches;
std::vector<EnvEntry> env_entries;
std::unordered_map<std::string, ServiceInit> service;
std::vector<Assertion> assertions;
std::unordered_set<std::string> const_keys;
Phase prepare;
Phase build;
Phase check;
Phase install;
};
} // namespace kappa::dsl
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <format>
#include <stdexcept>
#include <string>
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)) {}
};
inline int parse_int(int line, int col, const std::string& lexeme) {
try {
std::size_t pos = 0;
int val = std::stoi(lexeme, &pos);
if (pos != lexeme.size()) {
throw ParseError(line, col,
std::format("expected integer, got '{}'", lexeme));
}
return val;
} catch (const std::invalid_argument&) {
throw ParseError(line, col,
std::format("expected integer, got '{}'", lexeme));
} catch (const std::out_of_range&) {
throw ParseError(line, col,
std::format("integer out of range: '{}'", lexeme));
}
}
} // namespace kappa::dsl
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "kappa/dsl/token.hpp"
#include <string_view>
namespace kappa::dsl {
class Lexer {
public:
explicit Lexer(std::string_view source);
Token next();
private:
Token scan_ident();
Token scan_string();
Token scan_symbol();
void skip_whitespace();
bool skip_comment();
char peek() const;
char advance();
bool match(char c);
std::string_view source_;
std::size_t pos_ = 0;
int line_ = 1;
int col_ = 1;
int token_start_line_ = 0;
int token_start_col_ = 0;
};
} // namespace kappa::dsl
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include <string>
#include <string_view>
namespace kappa::dsl {
PackageDef parse(std::string_view source);
} // namespace kappa::dsl
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace kappa::dsl {
struct RollbackConfig {
int keep = 0;
};
struct SystemBlock {
std::string hostname;
std::string timezone;
std::vector<EnvEntry> env;
std::unordered_map<std::string, std::string> config;
std::unordered_map<std::string, FeatureDef> features;
RollbackConfig rollback;
};
struct PackageRef {
std::string name;
std::string version;
std::unordered_map<std::string, FeatureDef> features;
std::unordered_map<std::string, std::string> config;
};
struct BootBlock {
std::string kernel;
std::string init;
std::string efi;
std::string swap;
std::string root;
std::string bootloader;
std::unordered_map<std::string, std::string> params;
};
struct UserRef {
std::string name;
std::string shell;
std::vector<std::string> groups;
std::unordered_map<std::string, std::string> extra;
};
struct ServiceRef {
std::string name;
bool enable = false;
std::unordered_map<std::string, std::string> config;
};
struct SystemConfig {
std::vector<std::string> imports;
SystemBlock system;
std::vector<PackageRef> packages;
std::vector<ServiceRef> services;
BootBlock boot;
std::vector<UserRef> users;
std::vector<Assertion> assertions;
};
SystemConfig parse_system_config(std::string_view source);
SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_dir);
} // namespace kappa::dsl
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <string>
#include <string_view>
namespace kappa::dsl {
enum class TokenType {
Eof,
Newline,
// Symbols
Lbrace, // {
Rbrace, // }
Equals, // =
Lbracket, // [
Rbracket, // ]
Comma, // ,
// Literals
String, // "..."
Ident, // bare word (keyword, command, variable ref)
// Keywords (tokenised as Ident, then classified)
KwPackage,
KwVersion,
KwSource,
KwDepends,
KwProvides,
KwOutputs,
KwFeatures,
KwConfig,
KwConst,
KwLicense,
KwPatches,
KwEnv,
KwService,
KwAssert,
KwImport,
KwPrepare,
KwBuild,
KwCheck,
KwInstall,
KwTrue,
KwFalse,
};
struct Token {
TokenType type = TokenType::Eof;
std::string lexeme;
int line = 0;
int col = 0;
};
std::string_view token_name(TokenType type);
} // namespace kappa::dsl
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include <string>
#include <string_view>
#include <unordered_map>
namespace kappa::eval {
enum class VarKind {
Unknown,
Builtin, // ${prefix}, ${jobs}, ${jobopts}, ${userargs}
Package, // ${version}, ${name}, ${source}
Config, // ${cfg.key}
Feature, // ${feature.key} → expanded flag or ""
};
struct ResolvedVar {
std::string value;
VarKind kind = VarKind::Unknown;
};
struct Scope {
std::unordered_map<std::string, std::string> builtins;
std::unordered_map<std::string, std::string> config;
std::unordered_map<std::string, dsl::FeatureDef> features;
std::unordered_map<std::string, std::string> package;
};
Scope make_default_scope();
ResolvedVar resolve(std::string_view name, const Scope& scope);
std::string interpolate(std::string_view input, const Scope& scope);
} // namespace kappa::eval
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <filesystem>
namespace kappa::paths {
inline const std::filesystem::path root{"/kappa"};
inline const auto bin_dir = root / "bin";
inline const auto temp_dir = root / "temp";
inline const auto db_dir = root / "db";
inline const auto system_dir = root / "system";
inline const auto builds_dir = system_dir / "builds";
void ensure_directories();
} // namespace kappa::paths
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "binutils"
version = "2.46.1"
description = "GNU Binary Utilities (ld, as, objdump, readelf)"
[source]
url = "https://ftp.gnu.org/gnu/${name}/${name}-${version}.tar.xz" # ${} is string substitution
sha256 = "e127a709cba24c76de8936cb7083dd768f28cd37eb010492e2f19b71eb1294e4" # or sha512, or md5
[dependencies]
deps = ["zlib", "gettext"]
[build]
system = "autotools" # or cargo, make, cmake, or meson
+94
View File
@@ -0,0 +1,94 @@
#include "kappa/cli/diagnostic.hpp"
#include <algorithm>
#include <format>
#include <string_view>
#include <vector>
namespace kappa::cli {
using namespace std::string_view_literals;
static constexpr auto bold = "\033[1m"sv;
static constexpr auto red = "\033[31m"sv;
static constexpr auto cyan = "\033[36m"sv;
static constexpr auto yellow = "\033[33m"sv;
static constexpr auto reset = "\033[0m"sv;
static std::string_view line_at(std::string_view source, int target_line) {
int current = 1;
std::size_t start = std::string_view::npos;
for (std::size_t i = 0; i < source.size(); ++i) {
if (current == target_line) { start = i; break; }
if (source[i] == '\n') { ++current; }
}
if (start == std::string_view::npos) { return ""sv; }
auto end = source.find('\n', start);
if (end == std::string_view::npos) { end = source.size(); }
return source.substr(start, end - start);
}
static void print_header(std::ostream& os,
std::string_view severity,
std::string_view color,
std::string_view file,
int line, int col,
std::string_view message) {
os << color << bold << severity << ": " << reset
<< color << message << reset << '\n'
<< " " << cyan << "\u250C\u2500 " << file << ':' << line << ':' << col
<< reset << '\n';
}
void print_error(std::ostream& os,
std::string_view source,
SourceLocation loc,
std::string_view message) {
print_header(os, "error", red, loc.file, loc.line, loc.col, message);
auto line_src = line_at(source, loc.line);
if (!line_src.empty()) {
auto line_num = std::format("{}", loc.line);
os << " " << cyan << "\u2502" << reset << '\n'
<< ' ' << line_num << " " << cyan << "\u2502 " << reset
<< line_src << '\n';
os << " " << std::string(line_num.size(), ' ')
<< ' ' << cyan << "\u2502 " << reset;
for (int i = 0; i < loc.col - 1; ++i) { os << ' '; }
os << red << bold << '^' << reset << '\n';
}
os << " " << cyan << "\u2502" << reset << '\n';
}
void print_error(std::ostream& os,
std::string_view source,
SourceLocation loc,
std::string_view message,
std::string_view help) {
print_error(os, source, loc, message);
os << " " << cyan << "\u2570\u2500\u2500 " << reset
<< yellow << "help: " << reset << help << '\n';
}
void print_note(std::ostream& os,
std::string_view source,
SourceLocation loc,
std::string_view message) {
print_header(os, "note", cyan, loc.file, loc.line, loc.col, message);
auto line_src = line_at(source, loc.line);
if (!line_src.empty()) {
auto line_num = std::format("{}", loc.line);
os << " " << cyan << "\u2502" << reset << '\n'
<< ' ' << line_num << " " << cyan << "\u2502 " << reset
<< line_src << '\n'
<< " " << std::string(line_num.size(), ' ')
<< ' ' << cyan << "\u2502" << reset << '\n';
}
os << " " << cyan << "\u2570\u2500\u2500 " << reset
<< cyan << "note: " << reset << message << '\n';
}
} // namespace kappa::cli
+62
View File
@@ -0,0 +1,62 @@
#include "kappa/config/merge.hpp"
namespace kappa::config {
ResolvedPackage resolve_package(
const dsl::PackageDef& pkg,
const dsl::SystemBlock& system,
const std::optional<dsl::PackageRef>& overrides)
{
ResolvedPackage rp;
rp.original = pkg;
rp.features = pkg.features;
for (auto& [key, sys_feat] : system.features) {
auto it = rp.features.find(key);
if (it == rp.features.end()) {
rp.features[key] = sys_feat;
} else if (!it->second.force) {
it->second.enabled = sys_feat.enabled;
}
}
if (overrides) {
for (auto& [key, ovr_feat] : overrides->features) {
auto it = rp.features.find(key);
if (it == rp.features.end()) {
rp.features[key] = ovr_feat;
} else if (!it->second.force) {
it->second.enabled = ovr_feat.enabled;
}
}
}
rp.config = resolve_config(pkg, system, overrides);
return rp;
}
std::unordered_map<std::string, std::string> resolve_config(
const dsl::PackageDef& pkg,
const dsl::SystemBlock& system,
const std::optional<dsl::PackageRef>& overrides)
{
std::unordered_map<std::string, std::string> cfg = system.config;
for (auto& cf : pkg.config_files) {
for (auto& [key, val] : cf.entries) {
cfg.try_emplace(key, val);
}
}
if (overrides) {
for (auto& [key, val] : overrides->config) {
cfg[key] = val;
}
}
return cfg;
}
} // namespace kappa::config
+197
View File
@@ -0,0 +1,197 @@
#include "kappa/dsl/lexer.hpp"
#include <cctype>
#include <stdexcept>
#include <unordered_map>
namespace kappa::dsl {
static const std::unordered_map<std::string_view, TokenType> keywords = {
{"package", TokenType::KwPackage},
{"version", TokenType::KwVersion},
{"source", TokenType::KwSource},
{"depends", TokenType::KwDepends},
{"provides", TokenType::KwProvides},
{"outputs", TokenType::KwOutputs},
{"features", TokenType::KwFeatures},
{"config", TokenType::KwConfig},
{"const", TokenType::KwConst},
{"license", TokenType::KwLicense},
{"patches", TokenType::KwPatches},
{"env", TokenType::KwEnv},
{"service", TokenType::KwService},
{"assert", TokenType::KwAssert},
{"import", TokenType::KwImport},
{"prepare", TokenType::KwPrepare},
{"build", TokenType::KwBuild},
{"check", TokenType::KwCheck},
{"install", TokenType::KwInstall},
{"true", TokenType::KwTrue},
{"false", TokenType::KwFalse},
};
std::string_view token_name(TokenType type) {
switch (type) {
case TokenType::Eof: return "EOF";
case TokenType::Newline: return "newline";
case TokenType::Lbrace: return "{";
case TokenType::Rbrace: return "}";
case TokenType::Equals: return "=";
case TokenType::Lbracket: return "[";
case TokenType::Rbracket: return "]";
case TokenType::Comma: return ",";
case TokenType::String: return "string";
case TokenType::Ident: return "ident";
case TokenType::KwPackage: return "package";
case TokenType::KwVersion: return "version";
case TokenType::KwSource: return "source";
case TokenType::KwDepends: return "depends";
case TokenType::KwProvides: return "provides";
case TokenType::KwOutputs: return "outputs";
case TokenType::KwFeatures: return "features";
case TokenType::KwConfig: return "config";
case TokenType::KwConst: return "const";
case TokenType::KwLicense: return "license";
case TokenType::KwPatches: return "patches";
case TokenType::KwEnv: return "env";
case TokenType::KwService: return "service";
case TokenType::KwAssert: return "assert";
case TokenType::KwImport: return "import";
case TokenType::KwPrepare: return "prepare";
case TokenType::KwBuild: return "build";
case TokenType::KwCheck: return "check";
case TokenType::KwInstall: return "install";
case TokenType::KwTrue: return "true";
case TokenType::KwFalse: return "false";
}
return "?";
}
Lexer::Lexer(std::string_view source) : source_(source) {}
char Lexer::peek() const {
return pos_ < source_.size() ? source_[pos_] : '\0';
}
char Lexer::advance() {
if (pos_ >= source_.size()) { return '\0'; }
char c = source_[pos_++];
if (c == '\n') { ++line_; col_ = 1; }
else { ++col_; }
return c;
}
bool Lexer::match(char c) {
if (peek() == c) { advance(); return true; }
return false;
}
void Lexer::skip_whitespace() {
while (std::isspace(static_cast<unsigned char>(peek())) && peek() != '\n') {
advance();
}
}
bool Lexer::skip_comment() {
if (peek() != '/') { return false; }
if (pos_ + 1 >= source_.size()) { return false; }
char next = source_[pos_ + 1];
if (next == '/') {
advance(); advance(); // skip //
while (pos_ < source_.size() && peek() != '\n') { advance(); }
return true;
}
if (next == '*') {
advance(); advance(); // skip /*
while (pos_ + 1 < source_.size()) {
if (peek() == '*' && source_[pos_ + 1] == '/') {
advance(); advance(); // skip */
return true;
}
advance();
}
return true; // unterminated — consume to EOF
}
return false;
}
Token Lexer::next() {
while (true) {
skip_whitespace();
if (skip_comment()) { continue; }
token_start_line_ = line_;
token_start_col_ = col_;
if (pos_ >= source_.size()) { return {TokenType::Eof, "", line_, col_}; }
char c = peek();
if (c == '\n') {
advance();
return {TokenType::Newline, "\n", token_start_line_, token_start_col_};
}
if (c == '"') { return scan_string(); }
if (c == '{' || c == '}' || c == '=' || c == '[' || c == ']' || c == ',') {
return scan_symbol();
}
return scan_ident();
}
}
Token Lexer::scan_ident() {
std::string lexeme;
while (pos_ < source_.size()) {
char c = peek();
if (std::isspace(static_cast<unsigned char>(c))) { break; }
if (c == '"' || c == '[' || c == ']' || c == ',') { break; }
lexeme += advance();
}
auto it = keywords.find(lexeme);
if (it != keywords.end()) {
return {it->second, lexeme, token_start_line_, token_start_col_};
}
return {TokenType::Ident, lexeme, token_start_line_, token_start_col_};
}
Token Lexer::scan_string() {
advance(); // opening "
std::string lexeme;
while (pos_ < source_.size()) {
char c = peek();
if (c == '"') { advance(); break; }
if (c == '\\') {
advance();
if (pos_ < source_.size()) {
char esc = advance();
switch (esc) {
case 'n': lexeme += '\n'; break;
case 't': lexeme += '\t'; break;
case '\\': lexeme += '\\'; break;
case '"': lexeme += '"'; break;
default: lexeme += esc; break;
}
}
} else {
lexeme += advance();
}
}
return {TokenType::String, lexeme, token_start_line_, token_start_col_};
}
Token Lexer::scan_symbol() {
char c = advance();
switch (c) {
case '{': return {TokenType::Lbrace, "{", token_start_line_, token_start_col_};
case '}': return {TokenType::Rbrace, "}", token_start_line_, token_start_col_};
case '=': return {TokenType::Equals, "=", token_start_line_, token_start_col_};
case '[': return {TokenType::Lbracket, "[", token_start_line_, token_start_col_};
case ']': return {TokenType::Rbracket, "]", token_start_line_, token_start_col_};
case ',': return {TokenType::Comma, ",", token_start_line_, token_start_col_};
default: return {TokenType::Eof, "", token_start_line_, token_start_col_};
}
}
} // namespace kappa::dsl
+495
View File
@@ -0,0 +1,495 @@
#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
+439
View File
@@ -0,0 +1,439 @@
#include "kappa/dsl/system.hpp"
#include "kappa/dsl/error.hpp"
#include "kappa/dsl/lexer.hpp"
#include <filesystem>
#include <format>
#include <fstream>
#include <sstream>
#include <stdexcept>
namespace kappa::dsl {
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::KwService) ||
at(TokenType::KwAssert) ||
at(TokenType::KwImport) ||
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"; }
if (at(TokenType::Ident)) { auto v = current_.lexeme; advance(); return v; }
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 == "imports") {
consume(TokenType::Equals);
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
cfg.imports.push_back(consume(TokenType::String).lexeme);
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
} else if (kw == "assert") {
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(); // first =
if (at(TokenType::Equals)) {
a.op = "=="; advance(); // second =
} 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();
}
cfg.assertions.push_back(std::move(a));
skip_newlines();
}
consume(TokenType::Rbrace);
} else 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 = parse_int(current_.line, current_.col,
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 == "root") { cfg.boot.root = 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();
}
static void merge_config(SystemConfig& base, SystemConfig&& imported) {
if (!imported.system.hostname.empty()) { base.system.hostname = std::move(imported.system.hostname); }
if (!imported.system.timezone.empty()) { base.system.timezone = std::move(imported.system.timezone); }
for (auto& e : imported.system.env) { base.system.env.push_back(std::move(e)); }
for (auto& [k, v] : imported.system.config) { base.system.config[k] = std::move(v); }
for (auto& [k, v] : imported.system.features) { base.system.features[k] = std::move(v); }
if (imported.system.rollback.keep > 0) { base.system.rollback.keep = imported.system.rollback.keep; }
for (auto& p : imported.packages) { base.packages.push_back(std::move(p)); }
for (auto& s : imported.services) { base.services.push_back(std::move(s)); }
for (auto& u : imported.users) { base.users.push_back(std::move(u)); }
if (!imported.boot.kernel.empty()) { base.boot.kernel = std::move(imported.boot.kernel); }
if (!imported.boot.init.empty()) { base.boot.init = std::move(imported.boot.init); }
if (!imported.boot.efi.empty()) { base.boot.efi = std::move(imported.boot.efi); }
if (!imported.boot.swap.empty()) { base.boot.swap = std::move(imported.boot.swap); }
if (!imported.boot.root.empty()) { base.boot.root = std::move(imported.boot.root); }
if (!imported.boot.bootloader.empty()) { base.boot.bootloader = std::move(imported.boot.bootloader); }
for (auto& [k, v] : imported.boot.params) { base.boot.params[k] = std::move(v); }
}
SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_dir) {
auto resolved = cfg;
for (auto& import_path : cfg.imports) {
auto full_path = std::filesystem::path(base_dir) / import_path;
std::ifstream in(full_path);
if (!in) { continue; }
std::ostringstream buf;
buf << in.rdbuf();
auto imported = parse_system_config(buf.str());
merge_config(resolved, resolve_imports(imported, base_dir));
}
resolved.imports.clear();
return resolved;
}
} // namespace kappa::dsl
+82
View File
@@ -0,0 +1,82 @@
#include "kappa/eval/vars.hpp"
#include <string>
namespace kappa::eval {
Scope make_default_scope() {
Scope s;
s.builtins["prefix"] = "/usr";
s.builtins["jobs"] = "1";
s.builtins["jobopts"] = "-j1";
s.builtins["destdir"] = "/kappa/temp/destdir";
s.builtins["userargs"] = "";
return s;
}
ResolvedVar resolve(std::string_view name, const Scope& scope) {
auto dot = name.find('.');
if (dot == std::string_view::npos) {
if (auto it = scope.builtins.find(std::string(name)); it != scope.builtins.end()) {
return {it->second, VarKind::Builtin};
}
if (auto it = scope.package.find(std::string(name)); it != scope.package.end()) {
return {it->second, VarKind::Package};
}
return {{}, VarKind::Unknown};
}
auto ns = name.substr(0, dot);
auto key = name.substr(dot + 1);
if (ns == "cfg") {
if (auto it = scope.config.find(std::string(key)); it != scope.config.end()) {
return {it->second, VarKind::Config};
}
return {{}, VarKind::Unknown};
}
if (ns == "feature") {
if (auto it = scope.features.find(std::string(key)); it != scope.features.end()) {
const auto& f = it->second;
if (!f.enabled) { return {"", VarKind::Feature}; }
return {f.flag, VarKind::Feature};
}
return {{}, VarKind::Unknown};
}
return {{}, VarKind::Unknown};
}
std::string interpolate(std::string_view input, const Scope& scope) {
std::string result;
result.reserve(input.size());
std::size_t i = 0;
bool had_substitution = false;
while (i < input.size()) {
if (input[i] == '$' && i + 1 < input.size() && input[i + 1] == '{') {
auto end = input.find('}', i + 2);
if (end == std::string_view::npos) {
result += input.substr(i);
break;
}
auto var_name = input.substr(i + 2, end - (i + 2));
auto rv = resolve(var_name, scope);
result += rv.value;
had_substitution = true;
i = end + 1;
} else {
result += input[i];
++i;
}
}
if (had_substitution && result.contains("${")) {
return interpolate(result, scope);
}
return result;
}
} // namespace kappa::eval
+160
View File
@@ -0,0 +1,160 @@
#include "kappa/cli/diagnostic.hpp"
#include "kappa/dsl/parser.hpp"
#include "kappa/dsl/system.hpp"
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string_view>
using namespace std::string_view_literals;
using namespace kappa;
static constexpr auto version = "kappa 0.1.0"sv;
static void print_usage() {
std::cout << R"(kappa — a declarative source-based package manager
Usage:
kappa <subcommand> [options] <file>
Subcommands:
parse-package <file> Parse and validate a package definition (.kap)
parse-config <file> Parse and validate a system configuration
validate <file> Validate any kappa file (package or config)
Options:
-h, --help Show this help message
-V, --version Show version information
)";
}
static bool is_flag(std::string_view arg) {
return arg == "-h" || arg == "--help" || arg == "-V" || arg == "--version";
}
static std::string read_file(const char* path) {
std::ifstream in(path);
if (!in) {
std::cerr << "error: cannot open '" << path << "'\n";
std::exit(1);
}
std::ostringstream buf;
buf << in.rdbuf();
return buf.str();
}
static void handle_parse_error(const char* path,
std::string_view source,
const std::runtime_error& e) {
auto msg = std::string_view(e.what());
auto first_colon = msg.find(':');
auto second_colon = msg.find(':', first_colon + 1);
if (first_colon != std::string_view::npos &&
second_colon != std::string_view::npos) {
try {
int line = std::stoi(std::string(msg.substr(0, first_colon)));
int col = std::stoi(std::string(
msg.substr(first_colon + 1, second_colon - first_colon - 1)));
auto message = msg.substr(second_colon + 2);
cli::print_error(std::cerr, source,
{path, line, col}, message,
"check the syntax at this location");
return;
} catch (const std::exception&) {}
}
std::cerr << "error: " << e.what() << '\n';
}
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "kappa: missing subcommand\n\n";
print_usage();
return 1;
}
auto subcommand = std::string_view(argv[1]);
if (subcommand == "-h" || subcommand == "--help") {
print_usage();
return 0;
}
if (subcommand == "-V" || subcommand == "--version") {
std::cout << version << '\n';
return 0;
}
bool valid_subcommand = (subcommand == "parse-package")
|| (subcommand == "parse-config")
|| (subcommand == "validate");
if (!valid_subcommand) {
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
print_usage();
return 1;
}
// Find the file argument (skip flags)
const char* file_arg = nullptr;
for (int i = 2; i < argc; ++i) {
if (!is_flag(argv[i])) {
file_arg = argv[i];
break;
}
}
if (file_arg == nullptr) {
std::cerr << "error: no input file specified\n";
return 1;
}
auto source = read_file(file_arg);
if (subcommand == "parse-package") {
try {
auto pkg = dsl::parse(source);
std::cout << "package \"" << pkg.name << "\" " << pkg.version
<< " — valid\n";
return 0;
} catch (const std::runtime_error& e) {
handle_parse_error(file_arg, source, e);
return 1;
}
}
if (subcommand == "parse-config") {
try {
auto cfg = dsl::parse_system_config(source);
std::cout << "system config — valid ("
<< cfg.packages.size() << " packages, "
<< cfg.services.size() << " services, "
<< cfg.users.size() << " users)\n";
return 0;
} catch (const std::runtime_error& e) {
handle_parse_error(file_arg, source, e);
return 1;
}
}
if (subcommand == "validate") {
try {
dsl::parse(source);
std::cout << file_arg << ": valid package definition\n";
return 0;
} catch (const std::runtime_error&) {
try {
dsl::parse_system_config(source);
std::cout << file_arg << ": valid system configuration\n";
return 0;
} catch (const std::runtime_error& e) {
handle_parse_error(file_arg, source, e);
return 1;
}
}
}
return 1; // unreachable
}
+16
View File
@@ -0,0 +1,16 @@
#include "kappa/paths.hpp"
#include <system_error>
namespace kappa::paths {
void ensure_directories() {
std::error_code ec;
std::filesystem::create_directories(bin_dir, ec);
std::filesystem::create_directories(temp_dir, ec);
std::filesystem::create_directories(db_dir, ec);
std::filesystem::create_directories(builds_dir, ec);
}
} // namespace kappa::paths
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"name": "kappa",
"version": "0.1.0",
"dependencies": []
}