brewery
notashelf /
760094a2b70a3abacac0d386b19bd7274244782f

nixir

public

Import-resolving Nix IR plugin

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/nixir.git

Commit 760094a2b70a

tarball

NotAShelf <raf@notashelf.dev> · 2026-04-24 20:13 UTC

unverified4 files changed+235-9
irc: add flake reference support

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Icc96d297f02d3aad03b0373727d57f316a6a6964
diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cppindex 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<IREnvironment>(env));@@ -612,6 +642,27 @@ struct Evaluator::Impl {       } else {         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 {       v.mkNull();     }diff --git a/src/irc/ir_gen.cpp b/src/irc/ir_gen.cppindex 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<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{});   } };diff --git a/src/irc/main.cpp b/src/irc/main.cppindex 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 <cctype> #include <cstring>+#include <filesystem> #include <iostream>+#include <stdexcept> #include <string> #include <vector>  namespace nix_irc {+namespace fs = std::filesystem;  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"             << "  -I <path>    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<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) {   std::string input_file;   std::string output_file;@@ -57,19 +200,23 @@ 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<Node> ast; -    std::cout << "Parsing: " << input_file << "\n";-    auto ast = parser.parse_file(input_file);+    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);+    }      if (!ast) {       std::cerr << "Error: Failed to parse input\n";diff --git a/src/irc/serializer.cpp b/src/irc/serializer.cppindex 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<StringInterpolationNode>())       return NodeType::STRING_INTERPOLATION;+    if (node.holds<BuiltinCallNode>())+      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<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();       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: {       uint32_t index = read_u32();       return std::make_shared<Node>(VarNode(index, "", line));