irc: add flake reference support

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Icc96d297f02d3aad03b0373727d57f316a6a6964
This commit is contained in:
raf 2026-04-24 17:21:41 +03:00
commit 760094a2b7
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 235 additions and 9 deletions

View file

@ -69,6 +69,36 @@ struct Evaluator::Impl {
explicit Impl(EvalState& s) : state(s) {} explicit Impl(EvalState& s) : state(s) {}
static std::string escape_nix_string(std::string_view value) {
std::string escaped;
escaped.reserve(value.size());
for (char ch : value) {
switch (ch) {
case '\\':
escaped += "\\\\";
break;
case '"':
escaped += "\\\"";
break;
case '\n':
escaped += "\\n";
break;
case '\r':
escaped += "\\r";
break;
case '\t':
escaped += "\\t";
break;
default:
escaped.push_back(ch);
break;
}
}
return escaped;
}
IREnvironment* make_env(IREnvironment* parent = nullptr) { IREnvironment* make_env(IREnvironment* parent = nullptr) {
auto env = new IREnvironment(parent); auto env = new IREnvironment(parent);
environments.push_back(std::unique_ptr<IREnvironment>(env)); environments.push_back(std::unique_ptr<IREnvironment>(env));
@ -612,6 +642,27 @@ struct Evaluator::Impl {
} else { } else {
state.error<EvalError>("import argument must be a path or string").debugThrow(); state.error<EvalError>("import argument must be a path or string").debugThrow();
} }
} else if (auto* n = node->get_if<BuiltinCallNode>()) {
std::vector<Value*> args;
args.reserve(n->args.size());
for (const auto& arg_node : n->args) {
Value* arg = state.allocValue();
eval_node(arg_node, *arg, env);
args.push_back(arg);
}
if (n->builtin_name == "getFlake") {
if (args.size() != 1) {
state.error<EvalError>("getFlake expects exactly one argument").debugThrow();
}
auto flake_ref = state.forceStringNoCtx(*args[0], noPos, "while evaluating getFlake");
std::string expr = "builtins.getFlake \"" + escape_nix_string(flake_ref) + "\"";
auto* parsed = state.parseExprFromString(expr, state.rootPath(CanonPath::root));
state.eval(parsed, v);
} else {
state.error<EvalError>("unsupported builtin call: %s", n->builtin_name).debugThrow();
}
} else { } else {
v.mkNull(); v.mkNull();
} }

View file

@ -226,6 +226,14 @@ struct IRGenerator::Impl {
} }
return std::make_shared<Node>(ListNode(std::move(elements), n->line)); return std::make_shared<Node>(ListNode(std::move(elements), n->line));
} }
if (auto* n = node.get_if<BuiltinCallNode>()) {
std::vector<std::shared_ptr<Node>> args;
args.reserve(n->args.size());
for (const auto& arg : n->args) {
args.push_back(convert(arg));
}
return std::make_shared<Node>(BuiltinCallNode(n->builtin_name, std::move(args), n->line));
}
return std::make_shared<Node>(ConstNullNode{}); return std::make_shared<Node>(ConstNullNode{});
} }
}; };

View file

@ -1,22 +1,165 @@
#include "ir_gen.h" #include "ir_gen.h"
#include "parser.h" #include "parser.h"
#include "resolver.h"
#include "serializer.h" #include "serializer.h"
#include <cctype>
#include <cstring> #include <cstring>
#include <filesystem>
#include <iostream> #include <iostream>
#include <stdexcept>
#include <string> #include <string>
#include <vector> #include <vector>
namespace nix_irc { namespace nix_irc {
namespace fs = std::filesystem;
void print_usage(const char* prog) { void print_usage(const char* prog) {
std::cout << "Usage: " << prog << " [options] <input.nix> [output.nixir]\n" std::cout << "Usage: " << prog << " [options] <input.nix|flake#attr> [output.nixir]\n"
<< "\nOptions:\n" << "\nOptions:\n"
<< " -I <path> Add search path for imports\n" << " -I <path> Add search path for imports\n"
<< " --no-imports Disable import resolution\n" << " --no-imports Disable import resolution\n"
<< " --help Show this help\n"; << " --help Show this help\n";
} }
static bool is_flake_reference(const std::string& input) {
return input.find('#') != std::string::npos;
}
static std::string sanitize_output_stem(const std::string& input) {
std::string stem;
stem.reserve(input.size());
for (char ch : input) {
if (std::isalnum(static_cast<unsigned char>(ch))) {
stem.push_back(ch);
} else if (stem.empty() || stem.back() != '-') {
stem.push_back('-');
}
}
while (!stem.empty() && stem.back() == '-') {
stem.pop_back();
}
return stem.empty() ? "bundle" : stem;
}
static std::string default_output_path_for(const std::string& input) {
if (!is_flake_reference(input)) {
return input + "ir";
}
return sanitize_output_stem(input) + ".nixir";
}
static std::string normalize_local_flake_path(const std::string& raw_path) {
fs::path path = raw_path.empty() ? fs::current_path() : fs::path(raw_path);
fs::path absolute = path.is_absolute() ? path : fs::absolute(path);
fs::path normalized = absolute.lexically_normal();
if (!fs::exists(normalized)) {
throw std::runtime_error("Flake path does not exist: " + normalized.string());
}
if (fs::is_directory(normalized) && !fs::exists(normalized / "flake.nix")) {
throw std::runtime_error("Flake directory does not contain flake.nix: " + normalized.string());
}
return normalized.string();
}
static std::string normalize_flake_ref_source(const std::string& ref) {
if (ref.empty()) {
return normalize_local_flake_path(".");
}
if (ref.rfind("path:", 0) == 0) {
return "path:" + normalize_local_flake_path(ref.substr(5));
}
if (ref[0] == '.' || ref[0] == '/') {
return normalize_local_flake_path(ref);
}
if (fs::exists(ref)) {
return normalize_local_flake_path(ref);
}
return ref;
}
static std::vector<std::string> parse_flake_attr_path(const std::string& raw_attr_path) {
if (raw_attr_path.empty()) {
throw std::runtime_error("Flake reference is missing an attribute path after '#'");
}
std::vector<std::string> segments;
std::string current;
bool in_quotes = false;
bool escaping = false;
for (char ch : raw_attr_path) {
if (escaping) {
current.push_back(ch);
escaping = false;
continue;
}
if (in_quotes) {
if (ch == '\\') {
escaping = true;
} else if (ch == '"') {
in_quotes = false;
} else {
current.push_back(ch);
}
continue;
}
if (ch == '"') {
in_quotes = true;
} else if (ch == '.') {
if (current.empty()) {
throw std::runtime_error("Flake attribute path contains an empty segment");
}
segments.push_back(current);
current.clear();
} else {
current.push_back(ch);
}
}
if (escaping || in_quotes) {
throw std::runtime_error("Unterminated quoted segment in flake attribute path");
}
if (current.empty()) {
throw std::runtime_error("Flake attribute path contains an empty segment");
}
segments.push_back(current);
return segments;
}
static std::shared_ptr<Node> build_flake_ref_ast(const std::string& input) {
size_t hash_pos = input.find('#');
if (hash_pos == std::string::npos) {
throw std::runtime_error("Not a flake reference: " + input);
}
std::string flake_source = normalize_flake_ref_source(input.substr(0, hash_pos));
auto attr_path = parse_flake_attr_path(input.substr(hash_pos + 1));
auto expr = std::make_shared<Node>(BuiltinCallNode(
"getFlake",
std::vector<std::shared_ptr<Node>>{std::make_shared<Node>(ConstStringNode(flake_source))}));
for (const auto& attr : attr_path) {
expr = std::make_shared<Node>(SelectNode(expr, std::make_shared<Node>(ConstStringNode(attr))));
}
return expr;
}
int run_compile(int argc, char** argv) { int run_compile(int argc, char** argv) {
std::string input_file; std::string input_file;
std::string output_file; std::string output_file;
@ -57,20 +200,24 @@ int run_compile(int argc, char** argv) {
} }
if (output_file.empty()) { if (output_file.empty()) {
output_file = input_file + "ir"; output_file = default_output_path_for(input_file);
} }
try { try {
Parser parser; Parser parser;
Resolver resolver; (void) search_paths;
(void) resolve_imports;
for (const auto& path : search_paths) { std::shared_ptr<Node> ast;
resolver.add_search_path(path);
if (is_flake_reference(input_file)) {
std::cout << "Compiling flake reference: " << input_file << "\n";
ast = build_flake_ref_ast(input_file);
} else {
std::cout << "Parsing: " << input_file << "\n";
ast = parser.parse_file(input_file);
} }
std::cout << "Parsing: " << input_file << "\n";
auto ast = parser.parse_file(input_file);
if (!ast) { if (!ast) {
std::cerr << "Error: Failed to parse input\n"; std::cerr << "Error: Failed to parse input\n";
return 1; return 1;

View file

@ -78,6 +78,8 @@ struct Serializer::Impl {
return NodeType::LAMBDA_PATTERN; return NodeType::LAMBDA_PATTERN;
if (node.holds<StringInterpolationNode>()) if (node.holds<StringInterpolationNode>())
return NodeType::STRING_INTERPOLATION; return NodeType::STRING_INTERPOLATION;
if (node.holds<BuiltinCallNode>())
return NodeType::BUILTIN_CALL;
return NodeType::ERROR; return NodeType::ERROR;
} }
@ -250,6 +252,13 @@ struct Serializer::Impl {
write_node(*part.expr); write_node(*part.expr);
} }
} }
} else if (auto* n = node.get_if<BuiltinCallNode>()) {
write_string(n->builtin_name);
write_u32(n->args.size());
for (const auto& arg : n->args) {
if (arg)
write_node(*arg);
}
} }
} }
}; };
@ -366,6 +375,17 @@ struct Deserializer::Impl {
std::string val = read_string(); std::string val = read_string();
return std::make_shared<Node>(ConstLookupPathNode(val, line)); return std::make_shared<Node>(ConstLookupPathNode(val, line));
} }
case NodeType::BUILTIN_CALL: {
std::string builtin_name = read_string();
uint32_t num_args = read_u32();
std::vector<std::shared_ptr<Node>> args;
args.reserve(num_args);
for (uint32_t i = 0; i < num_args; i++) {
args.push_back(read_node());
}
return std::make_shared<Node>(
BuiltinCallNode(std::move(builtin_name), std::move(args), line));
}
case NodeType::VAR: { case NodeType::VAR: {
uint32_t index = read_u32(); uint32_t index = read_u32();
return std::make_shared<Node>(VarNode(index, "", line)); return std::make_shared<Node>(VarNode(index, "", line));