brewery
notashelf /
3387e0d822a24b85d7c7572e0b21b7dfd25d4230

nixir

public

Import-resolving Nix IR plugin

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

Commit 3387e0d822a2

tarball

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

unverified4 files changed+71-18
irc: support merge operator

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Icfb0cc81542e637d4b91c6a5788370fb6a6a6964
diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cppindex d30862c..d4ddab8 100644--- a/src/irc/evaluator.cpp+++ b/src/irc/evaluator.cpp@@ -117,13 +117,13 @@ struct Evaluator::Impl {       v.mkPath(state.rootPath(CanonPath(n->value)));     } else if (auto* n = node->get_if<ConstBoolNode>()) {       v.mkBool(n->value);-    } else if (auto* n = node->get_if<ConstNullNode>()) {  // NOLINT(bugprone-branch-clone)+    } else if (auto* n = node->get_if<ConstNullNode>()) { // NOLINT(bugprone-branch-clone)       v.mkNull();     } else if (auto* n = node->get_if<ConstURINode>()) {       // Parse and validate URI, then create string with URI context       auto parsed = parseURL(n->value, true);-      // Store URI with context - the parsed URL string-      v.mkString(parsed.to_string(), nix::NixStringContext{}, state.mem);+      // Store URI with context - use simple mkString with context+      v.mkString(parsed.to_string(), nix::NixStringContext{});     } else if (auto* n = node->get_if<VarNode>()) {       Value* bound = env ? env->lookup(n->index) : nullptr;       if (!bound && env && n->name.has_value()) {@@ -298,6 +298,32 @@ struct Evaluator::Impl {           // ++ is list concatenation in Nix; string concat uses ADD (+)           state.error<EvalError>("list concatenation not yet implemented").debugThrow();           break;+        case BinaryOp::MERGE: {+          // // is attrset merge - right overrides left+          if (left->type() != nAttrs || right->type() != nAttrs) {+            state.error<EvalError>("attrset merge requires two attrsets").debugThrow();+          }++          // Build a map of right attrs first (these have priority)+          std::unordered_map<Symbol, Value*> right_attrs;+          for (auto& attr : *right->attrs()) {+            right_attrs[attr.name] = attr.value;+          }++          // Copy right attrs to result+          auto builder = state.buildBindings(left->attrs()->size() + right->attrs()->size());+          for (auto& attr : *right->attrs()) {+            builder.insert(attr.name, attr.value);+          }+          // Add left attrs that don't exist in right+          for (auto& attr : *left->attrs()) {+            if (right_attrs.find(attr.name) == right_attrs.end()) {+              builder.insert(attr.name, attr.value);+            }+          }+          v.mkAttrs(builder.finish());+          break;+        }         default:           state.error<EvalError>("unknown binary operator").debugThrow();         }diff --git a/src/irc/ir_gen.cpp b/src/irc/ir_gen.cppindex a2561be..06318b4 100644--- a/src/irc/ir_gen.cpp+++ b/src/irc/ir_gen.cpp@@ -162,7 +162,8 @@ struct IRGenerator::Impl {         name_resolver.bind(key);       }       std::vector<std::pair<std::string, std::shared_ptr<Node>>> new_bindings;-      for (const auto& [key, val] : n->bindings) {+      new_bindings.reserve(n->bindings.size());+for (const auto& [key, val] : n->bindings) {         new_bindings.push_back({key, convert(val)});       }       auto body = convert(n->body);@@ -177,7 +178,8 @@ struct IRGenerator::Impl {         name_resolver.bind(key);       }       std::vector<std::pair<std::string, std::shared_ptr<Node>>> new_bindings;-      for (const auto& [key, val] : n->bindings) {+      new_bindings.reserve(n->bindings.size());+for (const auto& [key, val] : n->bindings) {         new_bindings.push_back({key, convert(val)});       }       auto body = convert(n->body);diff --git a/src/irc/parser.cpp b/src/irc/parser.cppindex bead1e5..b3edbfc 100644--- a/src/irc/parser.cpp+++ b/src/irc/parser.cpp@@ -95,6 +95,7 @@ struct Token {     STAR,     SLASH,     CONCAT,+    MERGE,     EQEQ,     NE,     LT,@@ -173,6 +174,10 @@ public:         tokens.push_back(TOKEN(CONCAT));         pos += 2;         col += 2;+      } else if (c == '/' && pos + 1 < input.size() && input[pos + 1] == '/') {+        tokens.push_back(TOKEN(MERGE));+        pos += 2;+        col += 2;       } else if (c == '&' && pos + 1 < input.size() && input[pos + 1] == '&') {         tokens.push_back(TOKEN(AND));         pos += 2;@@ -237,13 +242,13 @@ public:       } else if (isalpha(c)) {         // Check if it's a URI (contains ://) - look ahead         size_t lookahead = pos;-        while (lookahead < input.size() && (isalnum(input[lookahead]) || input[lookahead] == '_' ||-                                             input[lookahead] == '-' || input[lookahead] == '+' ||-                                             input[lookahead] == '.'))+        while (lookahead < input.size() &&+               (isalnum(input[lookahead]) || input[lookahead] == '_' || input[lookahead] == '-' ||+                input[lookahead] == '+' || input[lookahead] == '.'))           lookahead++;         std::string potential_scheme = input.substr(pos, lookahead - pos);-        if (lookahead + 2 < input.size() && input[lookahead] == ':' && input[lookahead + 1] == '/' &&-            input[lookahead + 2] == '/') {+        if (lookahead + 2 < input.size() && input[lookahead] == ':' &&+            input[lookahead + 1] == '/' && input[lookahead + 2] == '/') {           // It's a URI, consume the whole thing           tokenize_uri();         } else {@@ -267,7 +272,7 @@ private:   size_t line;   size_t col; -  void emit(Token t) {+  void emit(const Token& t) {     tokens.push_back(t);     pos++;     col++;@@ -394,7 +399,7 @@ private:   void tokenize_ident() {     size_t start = pos;     while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_' || input[pos] == '-' ||-                                   input[pos] == '+' || input[pos] == '.'))+                                  input[pos] == '+' || input[pos] == '.'))       pos++;     std::string ident = input.substr(start, pos - start); @@ -471,6 +476,8 @@ public:   // Get operator precedence (higher = tighter binding)   int get_precedence(Token::Type type) {     switch (type) {+    case Token::MERGE:+      return 1; // Low precedence - binds loosely, but must be > 0 to be recognized as binary op     case Token::OR:       return 1;     case Token::AND:@@ -511,6 +518,8 @@ public:       return BinaryOp::DIV;     case Token::CONCAT:       return BinaryOp::CONCAT;+    case Token::MERGE:+      return BinaryOp::MERGE;     case Token::EQEQ:       return BinaryOp::EQ;     case Token::NE:diff --git a/src/irc/types.h b/src/irc/types.hindex 5fb18f9..eb65734 100644--- a/src/irc/types.h+++ b/src/irc/types.h@@ -43,7 +43,23 @@ enum class NodeType : uint8_t {   ERROR = 0xFF }; -enum class BinaryOp : uint8_t { ADD, SUB, MUL, DIV, CONCAT, EQ, NE, LT, GT, LE, GE, AND, OR, IMPL };+enum class BinaryOp : uint8_t {+  ADD,+  SUB,+  MUL,+  DIV,+  CONCAT,+  EQ,+  NE,+  LT,+  GT,+  LE,+  GE,+  AND,+  OR,+  IMPL,+  MERGE+};  enum class UnaryOp : uint8_t { NEG, NOT }; @@ -203,11 +219,11 @@ struct ForceNode { // Node wraps a variant for type-safe AST 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>;+  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>;    Variant data;