brewery
notashelf /
775bb42c63250b3ee17c160d78dd492b50a1c9eb

nixir

public

Import-resolving Nix IR plugin

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

Commit 775bb42c6325

tarball

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

unverified4 files changed+116-60
irc: support lookup paths and `import` keyword

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0d16726646aef82ce675c4f8d209029a6a6a6964
diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cppindex d4ddab8..769897b 100644--- a/src/irc/evaluator.cpp+++ b/src/irc/evaluator.cpp@@ -5,10 +5,7 @@ #include "evaluator.h" #include "nix/expr/eval.hh" #include "nix/expr/value.hh"-#include "nix/util/error.hh" #include "nix/util/url.hh"--#include <stdexcept> #include <unordered_map>  namespace nix_irc {@@ -67,11 +64,7 @@ struct Evaluator::Impl {    explicit Impl(EvalState& s) : state(s) {} -  ~Impl() {-    for (auto& env : environments) {-      delete env.release();-    }-  }+  // Destructor not needed - unique_ptr handles cleanup automatically    IREnvironment* make_env(IREnvironment* parent = nullptr) {     auto env = new IREnvironment(parent);@@ -124,6 +117,11 @@ struct Evaluator::Impl {       auto parsed = parseURL(n->value, true);       // Store URI with context - use simple mkString with context       v.mkString(parsed.to_string(), nix::NixStringContext{});+    } else if (auto* n = node->get_if<ConstLookupPathNode>()) {+      // Lookup path like <nixpkgs>; resolve via Nix search path+      // We can use EvalState's searchPath to resolve+      auto path = state.findFile(n->value);+      v.mkPath(path);     } else if (auto* n = node->get_if<VarNode>()) {       Value* bound = env ? env->lookup(n->index) : nullptr;       if (!bound && env && n->name.has_value()) {@@ -369,17 +367,16 @@ struct Evaluator::Impl {     } else if (auto* n = node->get_if<LetNode>()) {       auto let_env = make_env(env);       for (const auto& [name, expr] : n->bindings) {-        Value* val = make_thunk(expr, env);+        // Create thunks in let_env so bindings can reference each other+        Value* val = make_thunk(expr, let_env);         let_env->bind(val);       }       eval_node(n->body, v, let_env);     } else if (auto* n = node->get_if<LetRecNode>()) {       auto letrec_env = make_env(env);-      std::vector<Value*> thunk_vals;        for (const auto& [name, expr] : n->bindings) {         Value* val = make_thunk(expr, letrec_env);-        thunk_vals.push_back(val);         letrec_env->bind(val);       } @@ -389,6 +386,8 @@ struct Evaluator::Impl {        IREnvironment* attr_env = env;       if (n->recursive) {+        // For recursive attrsets, create environment where all bindings can+        // see each other         attr_env = make_env(env);         for (const auto& [key, val] : n->attrs) {           Value* thunk = make_thunk(val, attr_env);@@ -396,13 +395,9 @@ struct Evaluator::Impl {         }       } +      // Attributes should be lazy, so store as thunks and not evaluated values       for (const auto& [key, val] : n->attrs) {-        Value* attr_val = state.allocValue();-        if (n->recursive) {-          eval_node(val, *attr_val, attr_env);-        } else {-          eval_node(val, *attr_val, env);-        }+        Value* attr_val = make_thunk(val, attr_env);         bindings.insert(state.symbols.create(key), attr_val);       } @@ -480,6 +475,21 @@ struct Evaluator::Impl {       }        eval_node(n->body, v, env);+    } else if (auto* n = node->get_if<ImportNode>()) {+      // Evaluate path expression to get the file path+      Value* path_val = state.allocValue();+      eval_node(n->path, *path_val, env);+      force(path_val);++      // Path should be a string or path type, convert to SourcePath+      if (path_val->type() == nPath) {+        state.evalFile(path_val->path(), v);+      } else if (path_val->type() == nString) {+        auto path = state.rootPath(CanonPath(path_val->c_str()));+        state.evalFile(path, v);+      } else {+        state.error<EvalError>("import argument must be a path or string").debugThrow();+      }     } else {       v.mkNull();     }diff --git a/src/irc/parser.cpp b/src/irc/parser.cppindex b3edbfc..43ebd1f 100644--- a/src/irc/parser.cpp+++ b/src/irc/parser.cpp@@ -4,8 +4,6 @@ #include <cstdlib> #include <iostream> #include <memory>-#include <regex>-#include <sstream> #include <stdexcept> #include <vector> @@ -24,22 +22,27 @@ static std::string read_file(const std::string& path) {   if (!f) {     throw std::runtime_error("Cannot open file: " + path);   }++  // Ensure FILE* is always closed+  auto file_closer = [](FILE* fp) {+    if (fp)+      fclose(fp);+  };+  std::unique_ptr<FILE, decltype(file_closer)> file_guard(f, file_closer);+   fseek(f, 0, SEEK_END);   long size = ftell(f);   fseek(f, 0, SEEK_SET);   std::string content(size, '\0');   if (fread(content.data(), 1, size, f) != static_cast<size_t>(size)) {-    fclose(f);     throw std::runtime_error("Failed to read file: " + path);   }-  fclose(f);   return content; }  static std::pair<std::string, std::string> run_command(const std::string& cmd) {   std::array<char, 256> buffer;   std::string result;-  std::string error;    FILE* pipe = popen(cmd.c_str(), "r");   if (!pipe)@@ -53,7 +56,7 @@ static std::pair<std::string, std::string> run_command(const std::string& cmd) {   if (status != 0) {     throw std::runtime_error("Command failed: " + cmd);   }-  return {result, error};+  return {result, ""}; }  struct Token {@@ -68,6 +71,7 @@ struct Token {     STRING,     STRING_INTERP,     PATH,+    LOOKUP_PATH,     INT,     FLOAT,     URI,@@ -81,6 +85,7 @@ struct Token {     ASSERT,     WITH,     INHERIT,+    IMPORT,     DOT,     SEMICOLON,     COLON,@@ -204,7 +209,29 @@ public:           emit(TOKEN(SLASH));         }       } else if (c == '<') {-        emit(TOKEN(LT));+        // Check for lookup path <nixpkgs> vs comparison operator+        size_t end = pos + 1;+        bool is_lookup_path = false;++        // Scan for valid lookup path characters until >+        while (end < input.size() &&+               (isalnum(input[end]) || input[end] == '-' || input[end] == '_' ||+                input[end] == '/' || input[end] == '.')) {+          end++;+        }++        // If we found > and there's content, it's a lookup path+        if (end < input.size() && input[end] == '>' && end > pos + 1) {+          std::string path = input.substr(pos + 1, end - pos - 1);+          tokens.push_back({Token::LOOKUP_PATH, path, line, col});+          pos = end + 1;+          col += (end - pos + 1);+          is_lookup_path = true;+        }++        if (!is_lookup_path) {+          emit(TOKEN(LT));+        }       } else if (c == '>') {         emit(TOKEN(GT));       } else if (c == '!') {@@ -430,6 +457,8 @@ private:       type = Token::WITH;     else if (ident == "inherit")       type = Token::INHERIT;+    else if (ident == "import")+      type = Token::IMPORT;     else if (ident == "true")       type = Token::BOOL;     else if (ident == "false")@@ -620,42 +649,18 @@ public:       if (name.type == Token::IDENT) {         advance();         auto attr = std::make_shared<Node>(ConstStringNode(name.value));-        auto result = std::make_shared<Node>(SelectNode(left, attr));--        if (consume(Token::DOT)) {-          Token name2 = current();-          if (name2.type == Token::IDENT) {-            advance();-            auto attr2 = std::make_shared<Node>(ConstStringNode(name2.value));-            auto* curr = result->get_if<SelectNode>();-            while (curr && consume(Token::DOT)) {-              Token n = current();-              expect(Token::IDENT);-              auto a = std::make_shared<Node>(ConstStringNode(n.value));-              curr->attr =-                  std::make_shared<Node>(AppNode(std::make_shared<Node>(AppNode(curr->attr, a)),-                                                 std::make_shared<Node>(ConstNullNode())));-            }-          }-        }-        return result;-      } else if (consume(Token::LBRACE)) {-        auto result = std::make_shared<Node>(-            SelectNode(left, std::make_shared<Node>(ConstStringNode(name.value))));-        parse_expr_attrs(result);-        expect(Token::RBRACE);-        return result;+        left = std::make_shared<Node>(SelectNode(left, attr));+        // Continue loop to handle multi-dot selections (a.b.c)+        continue;       }-      return left;+      // If we get here, the token after DOT was not IDENT or LBRACE+      // This is a parse error, but we'll just return what we have+      break;     }      return left;   } -  void parse_expr_attrs(std::shared_ptr<Node>&) {-    // Extended selection syntax-  }-   std::shared_ptr<Node> parse_expr2() {     std::shared_ptr<Node> left = parse_expr3(); @@ -679,6 +684,12 @@ public:   }    std::shared_ptr<Node> parse_expr3() {+    // Handle import expression+    if (consume(Token::IMPORT)) {+      auto path_expr = parse_expr3();+      return std::make_shared<Node>(ImportNode(path_expr));+    }+     // Handle unary operators     if (consume(Token::MINUS)) {       auto operand = parse_expr3();@@ -742,6 +753,11 @@ public:       return std::make_shared<Node>(ConstPathNode(t.value));     } +    if (t.type == Token::LOOKUP_PATH) {+      advance();+      return std::make_shared<Node>(ConstLookupPathNode(t.value));+    }+     if (t.type == Token::BOOL) {       advance();       return std::make_shared<Node>(ConstBoolNode(t.value == "true"));diff --git a/src/irc/serializer.cpp b/src/irc/serializer.cppindex 509735e..e1c3962 100644--- a/src/irc/serializer.cpp+++ b/src/irc/serializer.cpp@@ -1,7 +1,6 @@ #include "serializer.h" #include <cstring> #include <iostream>-#include <sstream>  namespace nix_irc { @@ -43,6 +42,8 @@ struct Serializer::Impl {       return NodeType::CONST_NULL;     if (node.holds<ConstURINode>())       return NodeType::CONST_URI;+    if (node.holds<ConstLookupPathNode>())+      return NodeType::CONST_LOOKUP_PATH;     if (node.holds<VarNode>())       return NodeType::VAR;     if (node.holds<LambdaNode>())@@ -53,6 +54,8 @@ struct Serializer::Impl {       return NodeType::BINARY_OP;     if (node.holds<UnaryOpNode>())       return NodeType::UNARY_OP;+    if (node.holds<ImportNode>())+      return NodeType::IMPORT;     if (node.holds<AttrsetNode>())       return NodeType::ATTRSET;     if (node.holds<SelectNode>())@@ -97,6 +100,8 @@ struct Serializer::Impl {       // No data for null     } else if (auto* n = node.get_if<ConstURINode>()) {       write_string(n->value);+    } else if (auto* n = node.get_if<ConstLookupPathNode>()) {+      write_string(n->value);     } else if (auto* n = node.get_if<VarNode>()) {       write_u32(n->index);     } else if (auto* n = node.get_if<LambdaNode>()) {@@ -118,6 +123,9 @@ struct Serializer::Impl {       write_u8(static_cast<uint8_t>(n->op));       if (n->operand)         write_node(*n->operand);+    } else if (auto* n = node.get_if<ImportNode>()) {+      if (n->path)+        write_node(*n->path);     } else if (auto* n = node.get_if<AttrsetNode>()) {       write_u8(n->recursive ? 1 : 0);       write_u32(n->attrs.size());@@ -289,6 +297,10 @@ struct Deserializer::Impl {       std::string val = read_string();       return std::make_shared<Node>(ConstURINode(val, line));     }+    case NodeType::CONST_LOOKUP_PATH: {+      std::string val = read_string();+      return std::make_shared<Node>(ConstLookupPathNode(val, line));+    }     case NodeType::VAR: {       uint32_t index = read_u32();       return std::make_shared<Node>(VarNode(index, "", line));@@ -314,6 +326,10 @@ struct Deserializer::Impl {       auto operand = read_node();       return std::make_shared<Node>(UnaryOpNode(op, operand, line));     }+    case NodeType::IMPORT: {+      auto path = read_node();+      return std::make_shared<Node>(ImportNode(path, line));+    }     case NodeType::ATTRSET: {       bool recursive = read_u8() != 0;       uint32_t num_attrs = read_u32();diff --git a/src/irc/types.h b/src/irc/types.hindex eb65734..a777a8c 100644--- a/src/irc/types.h+++ b/src/irc/types.h@@ -2,10 +2,8 @@ #define NIX_IRC_TYPES_H  #include <cstdint>-#include <fstream> #include <memory> #include <optional>-#include <sstream> #include <string> #include <unordered_map> #include <utility>@@ -25,11 +23,13 @@ enum class NodeType : uint8_t {   CONST_BOOL = 0x04,   CONST_NULL = 0x05,   CONST_URI = 0x07,+  CONST_LOOKUP_PATH = 0x08,   VAR = 0x10,   LAMBDA = 0x20,   APP = 0x21,   BINARY_OP = 0x22,   UNARY_OP = 0x23,+  IMPORT = 0x24,   ATTRSET = 0x30,   SELECT = 0x31,   HAS_ATTR = 0x34,@@ -107,6 +107,12 @@ struct ConstURINode {   ConstURINode(std::string v = "", uint32_t l = 0) : value(std::move(v)), line(l) {} }; +struct ConstLookupPathNode {+  std::string value; // e.g., "nixpkgs" or "nixpkgs/lib"+  uint32_t line = 0;+  ConstLookupPathNode(std::string v = "", uint32_t l = 0) : value(std::move(v)), line(l) {}+};+ struct VarNode {   uint32_t index = 0;   std::optional<std::string> name;@@ -204,6 +210,12 @@ struct AssertNode {   AssertNode(std::shared_ptr<Node> c, std::shared_ptr<Node> b, uint32_t l = 0); }; +struct ImportNode {+  std::shared_ptr<Node> path; // Path expression to import+  uint32_t line = 0;+  ImportNode(std::shared_ptr<Node> p, uint32_t l = 0);+};+ struct ThunkNode {   std::shared_ptr<Node> expr;   uint32_t line = 0;@@ -221,9 +233,9 @@ class Node { public:   using Variant =       std::variant<ConstIntNode, ConstFloatNode, ConstStringNode, ConstPathNode, ConstBoolNode,-                   ConstNullNode, ConstURINode, VarNode, LambdaNode, AppNode, BinaryOpNode,-                   UnaryOpNode, AttrsetNode, SelectNode, HasAttrNode, WithNode, IfNode, LetNode,-                   LetRecNode, AssertNode, ThunkNode, ForceNode>;+                   ConstNullNode, ConstURINode, ConstLookupPathNode, VarNode, LambdaNode, AppNode,+                   BinaryOpNode, UnaryOpNode, ImportNode, AttrsetNode, SelectNode, HasAttrNode,+                   WithNode, IfNode, LetNode, LetRecNode, AssertNode, ThunkNode, ForceNode>;    Variant data; @@ -270,6 +282,8 @@ inline LetRecNode::LetRecNode(std::shared_ptr<Node> b, uint32_t l) : body(std::m inline AssertNode::AssertNode(std::shared_ptr<Node> c, std::shared_ptr<Node> b, uint32_t l)     : cond(std::move(c)), body(std::move(b)), line(l) {} +inline ImportNode::ImportNode(std::shared_ptr<Node> p, uint32_t l) : path(std::move(p)), line(l) {}+ inline ThunkNode::ThunkNode(std::shared_ptr<Node> e, uint32_t l) : expr(std::move(e)), line(l) {}  inline ForceNode::ForceNode(std::shared_ptr<Node> e, uint32_t l) : expr(std::move(e)), line(l) {}