diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cpp index e053505..1684d40 100644 --- a/src/irc/evaluator.cpp +++ b/src/irc/evaluator.cpp @@ -69,6 +69,36 @@ struct Evaluator::Impl { 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) { auto env = new IREnvironment(parent); environments.push_back(std::unique_ptr(env)); @@ -612,6 +642,27 @@ struct Evaluator::Impl { } else { state.error("import argument must be a path or string").debugThrow(); } + } else if (auto* n = node->get_if()) { + std::vector 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("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("unsupported builtin call: %s", n->builtin_name).debugThrow(); + } } else { v.mkNull(); } diff --git a/src/irc/ir_gen.cpp b/src/irc/ir_gen.cpp index 90175f9..176ae4e 100644 --- a/src/irc/ir_gen.cpp +++ b/src/irc/ir_gen.cpp @@ -226,6 +226,14 @@ struct IRGenerator::Impl { } return std::make_shared(ListNode(std::move(elements), n->line)); } + if (auto* n = node.get_if()) { + std::vector> args; + args.reserve(n->args.size()); + for (const auto& arg : n->args) { + args.push_back(convert(arg)); + } + return std::make_shared(BuiltinCallNode(n->builtin_name, std::move(args), n->line)); + } return std::make_shared(ConstNullNode{}); } }; diff --git a/src/irc/main.cpp b/src/irc/main.cpp index 50c369a..3c00d70 100644 --- a/src/irc/main.cpp +++ b/src/irc/main.cpp @@ -1,22 +1,165 @@ #include "ir_gen.h" #include "parser.h" -#include "resolver.h" #include "serializer.h" +#include #include +#include #include +#include #include #include namespace nix_irc { +namespace fs = std::filesystem; void print_usage(const char* prog) { - std::cout << "Usage: " << prog << " [options] [output.nixir]\n" + std::cout << "Usage: " << prog << " [options] [output.nixir]\n" << "\nOptions:\n" << " -I Add search path for imports\n" << " --no-imports Disable import resolution\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(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 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 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 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(BuiltinCallNode( + "getFlake", + std::vector>{std::make_shared(ConstStringNode(flake_source))})); + + for (const auto& attr : attr_path) { + expr = std::make_shared(SelectNode(expr, std::make_shared(ConstStringNode(attr)))); + } + + return expr; +} + int run_compile(int argc, char** argv) { std::string input_file; std::string output_file; @@ -57,20 +200,24 @@ int run_compile(int argc, char** argv) { } if (output_file.empty()) { - output_file = input_file + "ir"; + output_file = default_output_path_for(input_file); } try { Parser parser; - Resolver resolver; + (void) search_paths; + (void) resolve_imports; - for (const auto& path : search_paths) { - resolver.add_search_path(path); + std::shared_ptr ast; + + 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) { std::cerr << "Error: Failed to parse input\n"; return 1; diff --git a/src/irc/serializer.cpp b/src/irc/serializer.cpp index 23cd392..a2c894a 100644 --- a/src/irc/serializer.cpp +++ b/src/irc/serializer.cpp @@ -78,6 +78,8 @@ struct Serializer::Impl { return NodeType::LAMBDA_PATTERN; if (node.holds()) return NodeType::STRING_INTERPOLATION; + if (node.holds()) + return NodeType::BUILTIN_CALL; return NodeType::ERROR; } @@ -250,6 +252,13 @@ struct Serializer::Impl { write_node(*part.expr); } } + } else if (auto* n = node.get_if()) { + 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(); return std::make_shared(ConstLookupPathNode(val, line)); } + case NodeType::BUILTIN_CALL: { + std::string builtin_name = read_string(); + uint32_t num_args = read_u32(); + std::vector> args; + args.reserve(num_args); + for (uint32_t i = 0; i < num_args; i++) { + args.push_back(read_node()); + } + return std::make_shared( + BuiltinCallNode(std::move(builtin_name), std::move(args), line)); + } case NodeType::VAR: { uint32_t index = read_u32(); return std::make_shared(VarNode(index, "", line));