From 38c13de01dfb8d11b08dd844d9bf2ba2ed97ab55 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 00:41:19 +0300 Subject: [PATCH 1/8] irc: add Float and URI literal support Signed-off-by: NotAShelf Change-Id: I40c59d94f650e7b9e68f77598492d7ab6a6a6964 --- src/irc/evaluator.cpp | 8 +++++ src/irc/parser.cpp | 81 +++++++++++++++++++++++++++++++++++++++--- src/irc/serializer.cpp | 21 +++++++++++ src/irc/types.h | 23 +++++++++--- tests/float_test.nix | 1 + tests/uri_test.nix | 1 + 6 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 tests/float_test.nix create mode 100644 tests/uri_test.nix diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cpp index dfd6eb1..d30862c 100644 --- a/src/irc/evaluator.cpp +++ b/src/irc/evaluator.cpp @@ -6,6 +6,7 @@ #include "nix/expr/eval.hh" #include "nix/expr/value.hh" #include "nix/util/error.hh" +#include "nix/util/url.hh" #include #include @@ -108,6 +109,8 @@ struct Evaluator::Impl { if (auto* n = node->get_if()) { v.mkInt(n->value); + } else if (auto* n = node->get_if()) { + v.mkFloat(n->value); } else if (auto* n = node->get_if()) { v.mkString(n->value); } else if (auto* n = node->get_if()) { @@ -116,6 +119,11 @@ struct Evaluator::Impl { v.mkBool(n->value); } else if (auto* n = node->get_if()) { // NOLINT(bugprone-branch-clone) v.mkNull(); + } else if (auto* n = node->get_if()) { + // 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); } else if (auto* n = node->get_if()) { Value* bound = env ? env->lookup(n->index) : nullptr; if (!bound && env && n->name.has_value()) { diff --git a/src/irc/parser.cpp b/src/irc/parser.cpp index e72d034..bead1e5 100644 --- a/src/irc/parser.cpp +++ b/src/irc/parser.cpp @@ -69,6 +69,8 @@ struct Token { STRING_INTERP, PATH, INT, + FLOAT, + URI, BOOL, LET, IN, @@ -216,14 +218,37 @@ public: } else if (c == '-') { // Check if it's a negative number or minus operator if (pos + 1 < input.size() && isdigit(input[pos + 1])) { - tokenize_int(); + // Check for negative float + if (pos + 2 < input.size() && input[pos + 2] == '.') { + tokenize_float(); + } else { + tokenize_int(); + } } else { emit(TOKEN(MINUS)); } } else if (isdigit(c)) { - tokenize_int(); - } else if (isalpha(c) || c == '_') { - tokenize_ident(); + // Check if it's a float (digit followed by '.') + if (pos + 1 < input.size() && input[pos + 1] == '.') { + tokenize_float(); + } else { + tokenize_int(); + } + } 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] == '.')) + lookahead++; + std::string potential_scheme = input.substr(pos, lookahead - pos); + if (lookahead + 2 < input.size() && input[lookahead] == ':' && input[lookahead + 1] == '/' && + input[lookahead + 2] == '/') { + // It's a URI, consume the whole thing + tokenize_uri(); + } else { + tokenize_ident(); + } } else { pos++; col++; @@ -339,12 +364,48 @@ private: col += num.size(); } + void tokenize_float() { + size_t start = pos; + if (input[pos] == '-') + pos++; + while (pos < input.size() && isdigit(input[pos])) + pos++; + if (pos < input.size() && input[pos] == '.') { + pos++; + while (pos < input.size() && isdigit(input[pos])) + pos++; + } + std::string num = input.substr(start, pos - start); + tokens.push_back({Token::FLOAT, num, line, col}); + col += num.size(); + } + + void tokenize_uri() { + size_t start = pos; + while (pos < input.size() && !isspace(input[pos]) && input[pos] != ')' && input[pos] != ']' && + input[pos] != ';') { + pos++; + } + std::string uri = input.substr(start, pos - start); + tokens.push_back({Token::URI, uri, line, col}); + col += uri.size(); + } + void tokenize_ident() { size_t start = pos; - while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_' || input[pos] == '-')) + while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_' || input[pos] == '-' || + input[pos] == '+' || input[pos] == '.')) pos++; std::string ident = input.substr(start, pos - start); + // Check if it's a URI (contains ://) + size_t scheme_end = ident.find("://"); + if (scheme_end != std::string::npos && scheme_end > 0) { + tokens.push_back({Token::URI, ident, line, col}); + col += ident.size(); + return; + } + Token::Type type = Token::IDENT; if (ident == "let") type = Token::LET; @@ -646,6 +707,16 @@ public: return std::make_shared(ConstIntNode(std::stoll(t.value))); } + if (t.type == Token::FLOAT) { + advance(); + return std::make_shared(ConstFloatNode(std::stod(t.value))); + } + + if (t.type == Token::URI) { + advance(); + return std::make_shared(ConstURINode(t.value)); + } + if (t.type == Token::STRING) { advance(); return std::make_shared(ConstStringNode(t.value)); diff --git a/src/irc/serializer.cpp b/src/irc/serializer.cpp index fff2208..509735e 100644 --- a/src/irc/serializer.cpp +++ b/src/irc/serializer.cpp @@ -31,6 +31,8 @@ struct Serializer::Impl { NodeType get_node_type(const Node& node) { if (node.holds()) return NodeType::CONST_INT; + if (node.holds()) + return NodeType::CONST_FLOAT; if (node.holds()) return NodeType::CONST_STRING; if (node.holds()) @@ -39,6 +41,8 @@ struct Serializer::Impl { return NodeType::CONST_BOOL; if (node.holds()) return NodeType::CONST_NULL; + if (node.holds()) + return NodeType::CONST_URI; if (node.holds()) return NodeType::VAR; if (node.holds()) @@ -78,6 +82,11 @@ struct Serializer::Impl { if (auto* n = node.get_if()) { write_u64(static_cast(n->value)); + } else if (auto* n = node.get_if()) { + double val = n->value; + uint64_t bits = 0; + std::memcpy(&bits, &val, sizeof(bits)); + write_u64(bits); } else if (auto* n = node.get_if()) { write_string(n->value); } else if (auto* n = node.get_if()) { @@ -86,6 +95,8 @@ struct Serializer::Impl { write_u8(n->value ? 1 : 0); } else if (auto* n = node.get_if()) { // No data for null + } else if (auto* n = node.get_if()) { + write_string(n->value); } else if (auto* n = node.get_if()) { write_u32(n->index); } else if (auto* n = node.get_if()) { @@ -254,6 +265,12 @@ struct Deserializer::Impl { int64_t val = static_cast(read_u64()); return std::make_shared(ConstIntNode(val, line)); } + case NodeType::CONST_FLOAT: { + uint64_t bits = read_u64(); + double val = 0.0; + std::memcpy(&val, &bits, sizeof(val)); + return std::make_shared(ConstFloatNode(val, line)); + } case NodeType::CONST_STRING: { std::string val = read_string(); return std::make_shared(ConstStringNode(val, line)); @@ -268,6 +285,10 @@ struct Deserializer::Impl { } case NodeType::CONST_NULL: return std::make_shared(ConstNullNode(line)); + case NodeType::CONST_URI: { + std::string val = read_string(); + return std::make_shared(ConstURINode(val, line)); + } case NodeType::VAR: { uint32_t index = read_u32(); return std::make_shared(VarNode(index, "", line)); diff --git a/src/irc/types.h b/src/irc/types.h index f52db8d..5fb18f9 100644 --- a/src/irc/types.h +++ b/src/irc/types.h @@ -19,10 +19,12 @@ constexpr uint32_t IR_VERSION = 2; enum class NodeType : uint8_t { CONST_INT = 0x01, + CONST_FLOAT = 0x06, CONST_STRING = 0x02, CONST_PATH = 0x03, CONST_BOOL = 0x04, CONST_NULL = 0x05, + CONST_URI = 0x07, VAR = 0x10, LAMBDA = 0x20, APP = 0x21, @@ -77,6 +79,18 @@ struct ConstNullNode { ConstNullNode(uint32_t l = 0) : line(l) {} }; +struct ConstFloatNode { + double value; + uint32_t line = 0; + ConstFloatNode(double v = 0.0, uint32_t l = 0) : value(v), line(l) {} +}; + +struct ConstURINode { + std::string value; + uint32_t line = 0; + ConstURINode(std::string v = "", uint32_t l = 0) : value(std::move(v)), line(l) {} +}; + struct VarNode { uint32_t index = 0; std::optional name; @@ -189,10 +203,11 @@ struct ForceNode { // Node wraps a variant for type-safe AST class Node { public: - using Variant = std::variant; + using Variant = std::variant; Variant data; diff --git a/tests/float_test.nix b/tests/float_test.nix new file mode 100644 index 0000000..c239c60 --- /dev/null +++ b/tests/float_test.nix @@ -0,0 +1 @@ +1.5 diff --git a/tests/uri_test.nix b/tests/uri_test.nix new file mode 100644 index 0000000..28ac1e2 --- /dev/null +++ b/tests/uri_test.nix @@ -0,0 +1 @@ +https://example.com/path?query=1#frag From 59fcd3ef92da0318e4f49d4414a8f82f3462cd06 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 01:37:35 +0300 Subject: [PATCH 2/8] irc: support merge operator Signed-off-by: NotAShelf Change-Id: Icfb0cc81542e637d4b91c6a5788370fb6a6a6964 --- src/irc/evaluator.cpp | 32 +++++++++++++++++++++++++++++--- src/irc/ir_gen.cpp | 6 ++++-- src/irc/parser.cpp | 23 ++++++++++++++++------- src/irc/types.h | 28 ++++++++++++++++++++++------ 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cpp index 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()) { v.mkBool(n->value); - } else if (auto* n = node->get_if()) { // NOLINT(bugprone-branch-clone) + } else if (auto* n = node->get_if()) { // NOLINT(bugprone-branch-clone) v.mkNull(); } else if (auto* n = node->get_if()) { // 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()) { 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("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("attrset merge requires two attrsets").debugThrow(); + } + + // Build a map of right attrs first (these have priority) + std::unordered_map 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("unknown binary operator").debugThrow(); } diff --git a/src/irc/ir_gen.cpp b/src/irc/ir_gen.cpp index 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>> 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>> 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.cpp index 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.h index 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; + using Variant = + std::variant; Variant data; From 3c1ce0fd31a3a25670f9b4ca3cac7bb1c802a5ba Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 12:47:58 +0300 Subject: [PATCH 3/8] tests: add test fixture for merge operator Signed-off-by: NotAShelf Change-Id: Ie8d8e5fb817349469fed194773120ce86a6a6964 --- tests/merge.nix | 2 ++ tests/uri_test.nix | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 tests/merge.nix diff --git a/tests/merge.nix b/tests/merge.nix new file mode 100644 index 0000000..221d0f6 --- /dev/null +++ b/tests/merge.nix @@ -0,0 +1,2 @@ +# Test attrset merge operator (//) +{a = {x = 1;} // {y = 2;};} diff --git a/tests/uri_test.nix b/tests/uri_test.nix index 28ac1e2..ad93389 100644 --- a/tests/uri_test.nix +++ b/tests/uri_test.nix @@ -1 +1,3 @@ -https://example.com/path?query=1#frag +https://example.com/path?query=1 +#frag + From a6aade6c1132fe031562c776c3dc9a5b6e8385c5 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 12:48:09 +0300 Subject: [PATCH 4/8] irc: support lookup paths and `import` keyword Signed-off-by: NotAShelf Change-Id: I0d16726646aef82ce675c4f8d209029a6a6a6964 --- src/irc/evaluator.cpp | 44 +++++++++++++-------- src/irc/parser.cpp | 90 +++++++++++++++++++++++++----------------- src/irc/serializer.cpp | 18 ++++++++- src/irc/types.h | 24 ++++++++--- 4 files changed, 116 insertions(+), 60 deletions(-) diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cpp index 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 #include 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()) { + // Lookup path like ; 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()) { 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()) { 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()) { auto letrec_env = make_env(env); - std::vector 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()) { + // 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("import argument must be a path or string").debugThrow(); + } } else { v.mkNull(); } diff --git a/src/irc/parser.cpp b/src/irc/parser.cpp index b3edbfc..43ebd1f 100644 --- a/src/irc/parser.cpp +++ b/src/irc/parser.cpp @@ -4,8 +4,6 @@ #include #include #include -#include -#include #include #include @@ -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_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)) { - fclose(f); throw std::runtime_error("Failed to read file: " + path); } - fclose(f); return content; } static std::pair run_command(const std::string& cmd) { std::array buffer; std::string result; - std::string error; FILE* pipe = popen(cmd.c_str(), "r"); if (!pipe) @@ -53,7 +56,7 @@ static std::pair 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 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(ConstStringNode(name.value)); - auto result = std::make_shared(SelectNode(left, attr)); - - if (consume(Token::DOT)) { - Token name2 = current(); - if (name2.type == Token::IDENT) { - advance(); - auto attr2 = std::make_shared(ConstStringNode(name2.value)); - auto* curr = result->get_if(); - while (curr && consume(Token::DOT)) { - Token n = current(); - expect(Token::IDENT); - auto a = std::make_shared(ConstStringNode(n.value)); - curr->attr = - std::make_shared(AppNode(std::make_shared(AppNode(curr->attr, a)), - std::make_shared(ConstNullNode()))); - } - } - } - return result; - } else if (consume(Token::LBRACE)) { - auto result = std::make_shared( - SelectNode(left, std::make_shared(ConstStringNode(name.value)))); - parse_expr_attrs(result); - expect(Token::RBRACE); - return result; + left = std::make_shared(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&) { - // Extended selection syntax - } - std::shared_ptr parse_expr2() { std::shared_ptr left = parse_expr3(); @@ -679,6 +684,12 @@ public: } std::shared_ptr parse_expr3() { + // Handle import expression + if (consume(Token::IMPORT)) { + auto path_expr = parse_expr3(); + return std::make_shared(ImportNode(path_expr)); + } + // Handle unary operators if (consume(Token::MINUS)) { auto operand = parse_expr3(); @@ -742,6 +753,11 @@ public: return std::make_shared(ConstPathNode(t.value)); } + if (t.type == Token::LOOKUP_PATH) { + advance(); + return std::make_shared(ConstLookupPathNode(t.value)); + } + if (t.type == Token::BOOL) { advance(); return std::make_shared(ConstBoolNode(t.value == "true")); diff --git a/src/irc/serializer.cpp b/src/irc/serializer.cpp index 509735e..e1c3962 100644 --- a/src/irc/serializer.cpp +++ b/src/irc/serializer.cpp @@ -1,7 +1,6 @@ #include "serializer.h" #include #include -#include namespace nix_irc { @@ -43,6 +42,8 @@ struct Serializer::Impl { return NodeType::CONST_NULL; if (node.holds()) return NodeType::CONST_URI; + if (node.holds()) + return NodeType::CONST_LOOKUP_PATH; if (node.holds()) return NodeType::VAR; if (node.holds()) @@ -53,6 +54,8 @@ struct Serializer::Impl { return NodeType::BINARY_OP; if (node.holds()) return NodeType::UNARY_OP; + if (node.holds()) + return NodeType::IMPORT; if (node.holds()) return NodeType::ATTRSET; if (node.holds()) @@ -97,6 +100,8 @@ struct Serializer::Impl { // No data for null } else if (auto* n = node.get_if()) { write_string(n->value); + } else if (auto* n = node.get_if()) { + write_string(n->value); } else if (auto* n = node.get_if()) { write_u32(n->index); } else if (auto* n = node.get_if()) { @@ -118,6 +123,9 @@ struct Serializer::Impl { write_u8(static_cast(n->op)); if (n->operand) write_node(*n->operand); + } else if (auto* n = node.get_if()) { + if (n->path) + write_node(*n->path); } else if (auto* n = node.get_if()) { 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(ConstURINode(val, line)); } + case NodeType::CONST_LOOKUP_PATH: { + std::string val = read_string(); + return std::make_shared(ConstLookupPathNode(val, line)); + } case NodeType::VAR: { uint32_t index = read_u32(); return std::make_shared(VarNode(index, "", line)); @@ -314,6 +326,10 @@ struct Deserializer::Impl { auto operand = read_node(); return std::make_shared(UnaryOpNode(op, operand, line)); } + case NodeType::IMPORT: { + auto path = read_node(); + return std::make_shared(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.h index eb65734..a777a8c 100644 --- a/src/irc/types.h +++ b/src/irc/types.h @@ -2,10 +2,8 @@ #define NIX_IRC_TYPES_H #include -#include #include #include -#include #include #include #include @@ -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 name; @@ -204,6 +210,12 @@ struct AssertNode { AssertNode(std::shared_ptr c, std::shared_ptr b, uint32_t l = 0); }; +struct ImportNode { + std::shared_ptr path; // Path expression to import + uint32_t line = 0; + ImportNode(std::shared_ptr p, uint32_t l = 0); +}; + struct ThunkNode { std::shared_ptr expr; uint32_t line = 0; @@ -221,9 +233,9 @@ class Node { public: using Variant = std::variant; + 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 b, uint32_t l) : body(std::m inline AssertNode::AssertNode(std::shared_ptr c, std::shared_ptr b, uint32_t l) : cond(std::move(c)), body(std::move(b)), line(l) {} +inline ImportNode::ImportNode(std::shared_ptr p, uint32_t l) : path(std::move(p)), line(l) {} + inline ThunkNode::ThunkNode(std::shared_ptr e, uint32_t l) : expr(std::move(e)), line(l) {} inline ForceNode::ForceNode(std::shared_ptr e, uint32_t l) : expr(std::move(e)), line(l) {} From 77aa67c7e0411d031e03078b86d3c3271cbed193 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 20:00:35 +0300 Subject: [PATCH 5/8] tests: add tests for lookup paths and imports Signed-off-by: NotAShelf Change-Id: I7e54691aa3e81efcb495124d13e8c24a6a6a6964 --- tests/import_lookup.nix | 3 + tests/import_simple.nix | 11 +++ tests/lookup_path.nix | 9 +++ tests/lookup_path_nested.nix | 3 + tests/regression_test.cpp | 125 +++++++++++++++++++++++++++++++++++ tests/string_interp.nix | 12 ++-- 6 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 tests/import_lookup.nix create mode 100644 tests/import_simple.nix create mode 100644 tests/lookup_path.nix create mode 100644 tests/lookup_path_nested.nix diff --git a/tests/import_lookup.nix b/tests/import_lookup.nix new file mode 100644 index 0000000..448b1ea --- /dev/null +++ b/tests/import_lookup.nix @@ -0,0 +1,3 @@ +# Test import with lookup path +# Common pattern: import { } +import diff --git a/tests/import_simple.nix b/tests/import_simple.nix new file mode 100644 index 0000000..023b49d --- /dev/null +++ b/tests/import_simple.nix @@ -0,0 +1,11 @@ +# Test import expression +# Import evaluates the file and returns its value + +# Import a file that returns a simple value (42) +import ./simple.nix + +# Can also import lookup paths: +# import { } + +# Import with path expressions: +# import (./dir + "/file.nix") diff --git a/tests/lookup_path.nix b/tests/lookup_path.nix new file mode 100644 index 0000000..e8bb4ca --- /dev/null +++ b/tests/lookup_path.nix @@ -0,0 +1,9 @@ +# Test lookup path syntax +# Lookup paths resolve via NIX_PATH environment variable +# Example: -> /nix/var/nix/profiles/per-user/root/channels/nixpkgs + +# Simple lookup path + + +# Nested lookup path (common pattern) +# diff --git a/tests/lookup_path_nested.nix b/tests/lookup_path_nested.nix new file mode 100644 index 0000000..0478b00 --- /dev/null +++ b/tests/lookup_path_nested.nix @@ -0,0 +1,3 @@ +# Test nested lookup path +# Common pattern in Nix: or + diff --git a/tests/regression_test.cpp b/tests/regression_test.cpp index 10123a0..1b4c4ce 100644 --- a/tests/regression_test.cpp +++ b/tests/regression_test.cpp @@ -157,6 +157,116 @@ void test_parser_expect_in_speculative_parsing() { << std::endl; } +void test_lookup_path_node() { + std::cout << "> Lookup path serialization..." << std::endl; + + auto lookup = std::make_shared(ConstLookupPathNode("nixpkgs")); + IRModule module; + module.entry = lookup; + + Serializer ser; + auto bytes = ser.serialize_to_bytes(module); + + Deserializer deser; + auto loaded = deser.deserialize(bytes); + + auto *loaded_lookup = loaded.entry->get_if(); + TEST_CHECK(loaded_lookup != nullptr, "Deserialized node is ConstLookupPathNode"); + TEST_CHECK(loaded_lookup && loaded_lookup->value == "nixpkgs", + "Lookup path value is 'nixpkgs'"); +} + +void test_import_node() { + std::cout << "> Import node serialization..." << std::endl; + + auto path = std::make_shared(ConstPathNode("./test.nix")); + auto import_node = std::make_shared(ImportNode(path)); + IRModule module; + module.entry = import_node; + + Serializer ser; + auto bytes = ser.serialize_to_bytes(module); + + Deserializer deser; + auto loaded = deser.deserialize(bytes); + + auto *loaded_import = loaded.entry->get_if(); + TEST_CHECK(loaded_import != nullptr, "Deserialized node is ImportNode"); + TEST_CHECK(loaded_import && loaded_import->path != nullptr, + "Import node has path"); + + if (loaded_import && loaded_import->path) { + auto *path_node = loaded_import->path->get_if(); + TEST_CHECK(path_node != nullptr, "Import path is ConstPathNode"); + TEST_CHECK(path_node && path_node->value == "./test.nix", + "Import path value is './test.nix'"); + } +} + +void test_import_with_lookup_path() { + std::cout << "> Import with lookup path..." << std::endl; + + auto lookup = std::make_shared(ConstLookupPathNode("nixpkgs")); + auto import_node = std::make_shared(ImportNode(lookup)); + IRModule module; + module.entry = import_node; + + Serializer ser; + auto bytes = ser.serialize_to_bytes(module); + + Deserializer deser; + auto loaded = deser.deserialize(bytes); + + auto *loaded_import = loaded.entry->get_if(); + TEST_CHECK(loaded_import != nullptr, "Deserialized node is ImportNode"); + + if (loaded_import && loaded_import->path) { + auto *lookup_node = loaded_import->path->get_if(); + TEST_CHECK(lookup_node != nullptr, "Import path is ConstLookupPathNode"); + TEST_CHECK(lookup_node && lookup_node->value == "nixpkgs", + "Lookup path value is 'nixpkgs'"); + } +} + +void test_uri_node() { + std::cout << "> URI node serialization..." << std::endl; + + auto uri = std::make_shared(ConstURINode("https://example.com")); + IRModule module; + module.entry = uri; + + Serializer ser; + auto bytes = ser.serialize_to_bytes(module); + + Deserializer deser; + auto loaded = deser.deserialize(bytes); + + auto *loaded_uri = loaded.entry->get_if(); + TEST_CHECK(loaded_uri != nullptr, "Deserialized node is ConstURINode"); + TEST_CHECK(loaded_uri && loaded_uri->value == "https://example.com", + "URI value is 'https://example.com'"); +} + +void test_float_node() { + std::cout << "> Float node serialization..." << std::endl; + + auto float_val = std::make_shared(ConstFloatNode(3.14159)); + IRModule module; + module.entry = float_val; + + Serializer ser; + auto bytes = ser.serialize_to_bytes(module); + + Deserializer deser; + auto loaded = deser.deserialize(bytes); + + auto *loaded_float = loaded.entry->get_if(); + TEST_CHECK(loaded_float != nullptr, "Deserialized node is ConstFloatNode"); + TEST_CHECK(loaded_float && loaded_float->value > 3.14 && + loaded_float->value < 3.15, + "Float value is approximately 3.14159"); +} + int main() { std::cout << "=== Regression Tests for Nixir ===" << std::endl << std::endl; @@ -178,6 +288,21 @@ int main() { test_parser_expect_in_speculative_parsing(); std::cout << std::endl; + test_lookup_path_node(); + std::cout << std::endl; + + test_import_node(); + std::cout << std::endl; + + test_import_with_lookup_path(); + std::cout << std::endl; + + test_uri_node(); + std::cout << std::endl; + + test_float_node(); + std::cout << std::endl; + std::cout << "=== Tests Complete ===" << std::endl; std::cout << "Failures: " << failures << std::endl; return failures > 0 ? 1 : 0; diff --git a/tests/string_interp.nix b/tests/string_interp.nix index af7b42d..9edc11b 100644 --- a/tests/string_interp.nix +++ b/tests/string_interp.nix @@ -10,13 +10,11 @@ in { # Multiple interpolations multi = "x is ${x} and name is ${name}"; - # Nested expression - nested = "Result: ${ - if bool_val - then "yes" - else "no" - }"; + # Expression evaluation in interpolation + computed = "x + 10 = ${x + 10}"; - # Just a string (no interpolation) + bool_check = "${bool_val} is true!"; + + # Just a string, no interpolation plain = "plain text"; } From ed8f637c991c9f3e80b610ded37c6123b60bdd03 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 20:03:25 +0300 Subject: [PATCH 6/8] irc: more syntax support Indented strings, ancient let bindings and a bit more Signed-off-by: NotAShelf Change-Id: Ib86c2d8ca4402dfa0c5c536a9959f4006a6a6964 --- src/irc/evaluator.cpp | 35 +++++- src/irc/ir_gen.cpp | 4 +- src/irc/parser.cpp | 286 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 313 insertions(+), 12 deletions(-) diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cpp index 769897b..6004f01 100644 --- a/src/irc/evaluator.cpp +++ b/src/irc/evaluator.cpp @@ -107,7 +107,15 @@ struct Evaluator::Impl { } else if (auto* n = node->get_if()) { v.mkString(n->value); } else if (auto* n = node->get_if()) { - v.mkPath(state.rootPath(CanonPath(n->value))); + std::string path = n->value; + // Expand ~/ to home directory + if (path.size() >= 2 && path[0] == '~' && path[1] == '/') { + const char* home = getenv("HOME"); + if (home) { + path = std::string(home) + path.substr(1); + } + } + v.mkPath(state.rootPath(CanonPath(path))); } else if (auto* n = node->get_if()) { v.mkBool(n->value); } else if (auto* n = node->get_if()) { // NOLINT(bugprone-branch-clone) @@ -222,6 +230,22 @@ struct Evaluator::Impl { v.mkInt((left->integer() + right->integer()).valueWrapping()); } else if (left->type() == nString && right->type() == nString) { v.mkString(std::string(left->c_str()) + std::string(right->c_str())); + } else if (left->type() == nPath && right->type() == nString) { + // Path + string = path + std::string leftPath = std::string(left->path().path.abs()); + std::string result = leftPath + std::string(right->c_str()); + v.mkPath(state.rootPath(CanonPath(result))); + } else if (left->type() == nString && right->type() == nPath) { + // String + path = path + std::string rightPath = std::string(right->path().path.abs()); + std::string result = std::string(left->c_str()) + rightPath; + v.mkPath(state.rootPath(CanonPath(result))); + } else if (left->type() == nPath && right->type() == nPath) { + // Path + path = path + std::string leftPath = std::string(left->path().path.abs()); + std::string rightPath = std::string(right->path().path.abs()); + std::string result = leftPath + rightPath; + v.mkPath(state.rootPath(CanonPath(result))); } else { state.error("type error in addition").debugThrow(); } @@ -293,8 +317,13 @@ struct Evaluator::Impl { } break; case BinaryOp::CONCAT: - // ++ is list concatenation in Nix; string concat uses ADD (+) - state.error("list concatenation not yet implemented").debugThrow(); + // TODO: ++ list concatenation requires accessing private Nix Value payload + // For now, delegate to Nix's concatLists or implement via builtins + // Parser recognizes ++ but evaluator not yet fully implemented + state + .error( + "list concatenation (++) not yet fully implemented - use builtins.concatLists") + .debugThrow(); break; case BinaryOp::MERGE: { // // is attrset merge - right overrides left diff --git a/src/irc/ir_gen.cpp b/src/irc/ir_gen.cpp index 06318b4..95999c8 100644 --- a/src/irc/ir_gen.cpp +++ b/src/irc/ir_gen.cpp @@ -163,7 +163,7 @@ struct IRGenerator::Impl { } std::vector>> new_bindings; new_bindings.reserve(n->bindings.size()); -for (const auto& [key, val] : n->bindings) { + for (const auto& [key, val] : n->bindings) { new_bindings.push_back({key, convert(val)}); } auto body = convert(n->body); @@ -179,7 +179,7 @@ for (const auto& [key, val] : n->bindings) { } std::vector>> new_bindings; new_bindings.reserve(n->bindings.size()); -for (const auto& [key, val] : n->bindings) { + 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.cpp index 43ebd1f..3780fb7 100644 --- a/src/irc/parser.cpp +++ b/src/irc/parser.cpp @@ -70,6 +70,8 @@ struct Token { IDENT, STRING, STRING_INTERP, + INDENTED_STRING, + INDENTED_STRING_INTERP, PATH, LOOKUP_PATH, INT, @@ -153,6 +155,8 @@ public: emit(TOKEN(AT)); } else if (c == ',') { emit(TOKEN(COMMA)); + } else if (c == '\'' && pos + 1 < input.size() && input[pos + 1] == '\'') { + tokenize_indented_string(); } else if (c == '"') { tokenize_string(); } @@ -247,6 +251,14 @@ public: } } else if (c == '?') { emit(TOKEN(QUESTION)); + } else if (c == '~') { + // Home-relative path ~/... + if (pos + 1 < input.size() && input[pos + 1] == '/') { + tokenize_home_path(); + } else { + // Just ~ by itself is an identifier + tokenize_ident(); + } } else if (c == '-') { // Check if it's a negative number or minus operator if (pos + 1 < input.size() && isdigit(input[pos + 1])) { @@ -317,8 +329,26 @@ private: } pos++; } else if (c == '#') { + // Line comment - skip until newline while (pos < input.size() && input[pos] != '\n') pos++; + } else if (c == '/' && pos + 1 < input.size() && input[pos + 1] == '*') { + // Block comment /* ... */ + // Note: Nix block comments do NOT nest + pos += 2; // Skip /* + while (pos + 1 < input.size()) { + if (input[pos] == '*' && input[pos + 1] == '/') { + pos += 2; // Skip */ + break; + } + if (input[pos] == '\n') { + line++; + col = 1; + } else { + col++; + } + pos++; + } } else { break; } @@ -374,10 +404,131 @@ private: col += s.size() + 2; } + void tokenize_indented_string() { + pos += 2; // Skip opening '' + std::string raw_content; + bool has_interp = false; + size_t start_line = line; + + // Collect raw content until closing '' + while (pos < input.size()) { + // Check for escape sequences + if (pos + 1 < input.size() && input[pos] == '\'' && input[pos + 1] == '\'') { + // Check if it's an escape or the closing delimiter + if (pos + 2 < input.size() && input[pos + 2] == '\'') { + // ''' -> escape for '' + raw_content += "''"; + pos += 3; + continue; + } else if (pos + 2 < input.size() && input[pos + 2] == '$') { + // ''$ -> escape for $ + raw_content += '$'; + pos += 3; + continue; + } else if (pos + 2 < input.size() && input[pos + 2] == '\\') { + // ''\ -> escape for backslash + raw_content += '\\'; + pos += 3; + continue; + } else { + // Just closing '' + pos += 2; + break; + } + } + + // Check for interpolation + if (input[pos] == '$' && pos + 1 < input.size() && input[pos + 1] == '{') { + has_interp = true; + raw_content += input[pos]; + pos++; + if (input[pos] == '\n') { + line++; + } + continue; + } + + // Track newlines + if (input[pos] == '\n') { + line++; + raw_content += input[pos]; + pos++; + } else { + raw_content += input[pos]; + pos++; + } + } + + // Strip common indentation + std::string stripped = strip_indentation(raw_content); + + Token::Type type = has_interp ? Token::INDENTED_STRING_INTERP : Token::INDENTED_STRING; + tokens.push_back({type, stripped, start_line, col}); + } + + std::string strip_indentation(const std::string& s) { + if (s.empty()) + return s; + + // Split into lines + std::vector lines; + std::string current_line; + for (char c : s) { + if (c == '\n') { + lines.push_back(current_line); + current_line.clear(); + } else { + current_line += c; + } + } + if (!current_line.empty() || (!s.empty() && s.back() == '\n')) { + lines.push_back(current_line); + } + + // Find minimum indentation (spaces/tabs at start of non-empty lines) + size_t min_indent = std::string::npos; + for (const auto& line : lines) { + if (line.empty()) + continue; // Skip empty lines when calculating indentation + size_t indent = 0; + for (char c : line) { + if (c == ' ' || c == '\t') + indent++; + else + break; + } + if (indent < min_indent) + min_indent = indent; + } + + if (min_indent == std::string::npos) + min_indent = 0; + + // Strip min_indent from all lines + std::string result; + for (size_t i = 0; i < lines.size(); i++) { + const auto& line = lines[i]; + if (line.empty()) { + // Preserve empty lines + if (i + 1 < lines.size()) + result += '\n'; + } else { + // Strip indentation + size_t skip = std::min(min_indent, line.size()); + result += line.substr(skip); + if (i + 1 < lines.size()) + result += '\n'; + } + } + + return result; + } + void tokenize_path() { size_t start = pos; while (pos < input.size() && !isspace(input[pos]) && input[pos] != '(' && input[pos] != ')' && - input[pos] != '{' && input[pos] != '}' && input[pos] != '[' && input[pos] != ']') { + input[pos] != '{' && input[pos] != '}' && input[pos] != '[' && input[pos] != ']' && + input[pos] != ';') { pos++; } std::string path = input.substr(start, pos - start); @@ -385,6 +536,22 @@ private: col += path.size(); } + void tokenize_home_path() { + size_t start = pos; + pos++; // Skip ~ + if (pos < input.size() && input[pos] == '/') { + // Home-relative path ~/something + while (pos < input.size() && !isspace(input[pos]) && input[pos] != '(' && input[pos] != ')' && + input[pos] != '{' && input[pos] != '}' && input[pos] != '[' && input[pos] != ']' && + input[pos] != ';') { + pos++; + } + } + std::string path = input.substr(start, pos - start); + tokens.push_back({Token::PATH, path, line, col}); + col += path.size(); + } + void tokenize_int() { size_t start = pos; if (input[pos] == '-') @@ -587,6 +754,45 @@ public: return std::make_shared(IfNode(cond, then, else_)); } if (consume(Token::LET)) { + // Check for ancient let syntax: let { x = 1; body = x; } + if (current().type == Token::LBRACE) { + advance(); // consume { + std::vector>> bindings; + std::shared_ptr body_expr; + + while (current().type != Token::RBRACE && current().type != Token::EOF_) { + if (current().type != Token::IDENT && current().type != Token::STRING && + current().type != Token::INDENTED_STRING) { + throw std::runtime_error("Expected identifier in ancient let"); + } + + std::string name = current().value; + advance(); + expect(Token::EQUALS); + auto value = parse_expr(); + expect(Token::SEMICOLON); + + // Check if this is the special 'body' binding + if (name == "body") { + body_expr = value; + } else { + bindings.push_back({name, value}); + } + } + + expect(Token::RBRACE); + + if (!body_expr) { + throw std::runtime_error("Ancient let syntax requires 'body' attribute"); + } + + // Ancient let is always recursive + auto letrec = LetRecNode(body_expr); + letrec.bindings = std::move(bindings); + return std::make_shared(std::move(letrec)); + } + + // Modern let syntax: let x = 1; in x bool is_rec = consume(Token::REC); std::vector>> bindings; parse_bindings(bindings); @@ -653,11 +859,23 @@ public: // Continue loop to handle multi-dot selections (a.b.c) continue; } - // If we get here, the token after DOT was not IDENT or LBRACE + // If we get here, the token after DOT was not IDENT // This is a parse error, but we'll just return what we have break; } + // Check for 'or' default value: a.b or default + // This is checked after all selections, so works for any selection depth + // 'or' is contextual - only special after a selection expression + if (left->get_if() && current().type == Token::IDENT && current().value == "or") { + advance(); + // Parse default as a primary expression + auto default_expr = parse_expr3(); + // Update the SelectNode with the default expression + auto* select = left->get_if(); + select->default_expr = default_expr; + } + return left; } @@ -748,6 +966,17 @@ public: return parse_string_interp(str_token.value); } + if (t.type == Token::INDENTED_STRING) { + advance(); + return std::make_shared(ConstStringNode(t.value)); + } + + if (t.type == Token::INDENTED_STRING_INTERP) { + Token str_token = current(); + advance(); + return parse_string_interp(str_token.value); + } + if (t.type == Token::PATH) { advance(); return std::make_shared(ConstPathNode(t.value)); @@ -808,18 +1037,61 @@ public: continue; } - if (current().type == Token::IDENT || current().type == Token::STRING) { - Token key = current(); + // Check for dynamic attribute name: ${expr} = value + if (current().type == Token::STRING_INTERP || + current().type == Token::INDENTED_STRING_INTERP) { + Token str_token = current(); advance(); - std::string key_str = key.value; + auto name_expr = parse_string_interp(str_token.value); if (consume(Token::EQUALS)) { auto value = parse_expr(); - attrs.attrs.push_back({key_str, value}); + // For dynamic attrs, we use special marker in key and store expr as value + // This will need runtime evaluation - store as special node + // For now, convert to string at parse time if possible + // TODO: Full dynamic attr support needs IR node for dynamic keys + attrs.attrs.push_back({"__dynamic__", value}); + } + } else if (current().type == Token::IDENT || current().type == Token::STRING || + current().type == Token::INDENTED_STRING) { + // Parse attribute path: a.b.c = value + std::vector path; + path.push_back(current().value); + advance(); + + // Collect dot-separated path components + while (consume(Token::DOT)) { + if (current().type == Token::IDENT || current().type == Token::STRING || + current().type == Token::INDENTED_STRING) { + path.push_back(current().value); + advance(); + } else { + break; + } + } + + if (consume(Token::EQUALS)) { + auto value = parse_expr(); + + // Desugar nested paths: a.b.c = v becomes a = { b = { c = v; }; } + if (path.size() == 1) { + // Simple case: just one key + attrs.attrs.push_back({path[0], value}); + } else { + // Nested case: build nested attrsets from right to left + auto nested = value; + for (int i = path.size() - 1; i > 0; i--) { + auto inner_attrs = AttrsetNode(false); + inner_attrs.attrs.push_back({path[i], nested}); + nested = std::make_shared(std::move(inner_attrs)); + } + attrs.attrs.push_back({path[0], nested}); + } } else if (consume(Token::AT)) { + // @ pattern - not affected by nested paths auto pattern = parse_expr(); auto value = parse_expr(); - attrs.attrs.push_back({key_str, value}); + attrs.attrs.push_back({path[0], value}); } } From 00a3d2e585bfa011308812cfe1f00bf8e6362118 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 21:49:12 +0300 Subject: [PATCH 7/8] tests: update test cases for newer syntax items; drop old artifacts Signed-off-by: NotAShelf Change-Id: I8640148e8e7597924f9c776750c856266a6a6964 --- tests/ancient_let.nix | 8 ++++++++ tests/attrset.nixir | Bin 109 -> 0 bytes tests/block_comments.nix | 12 ++++++++++++ tests/comparison.nixir | Bin 107 -> 0 bytes tests/dynamic_attrs.nix | 15 +++++++++++++++ tests/home_path.nix | 11 +++++++++++ tests/if.nixir | Bin 58 -> 0 bytes tests/indented_string.nix | 31 +++++++++++++++++++++++++++++++ tests/let.nixir | Bin 75 -> 0 bytes tests/list_concat.nix | 15 +++++++++++++++ tests/logical.nixir | Bin 149 -> 0 bytes tests/nested_attrs.nix | 13 +++++++++++++ tests/operators.nixir | Bin 109 -> 0 bytes tests/or_in_attrset.nix | 6 ++++++ tests/or_simple.nix | 4 ++++ tests/path_concat.nix | 13 +++++++++++++ tests/precedence.nixir | Bin 318 -> 0 bytes tests/select_or_default.nix | 16 ++++++++++++++++ tests/simple.nixir | Bin 34 -> 0 bytes tests/simple_op.nixir | Bin 53 -> 0 bytes tests/unary.nixir | Bin 113 -> 0 bytes 21 files changed, 144 insertions(+) create mode 100644 tests/ancient_let.nix delete mode 100644 tests/attrset.nixir create mode 100644 tests/block_comments.nix delete mode 100644 tests/comparison.nixir create mode 100644 tests/dynamic_attrs.nix create mode 100644 tests/home_path.nix delete mode 100644 tests/if.nixir create mode 100644 tests/indented_string.nix delete mode 100644 tests/let.nixir create mode 100644 tests/list_concat.nix delete mode 100644 tests/logical.nixir create mode 100644 tests/nested_attrs.nix delete mode 100644 tests/operators.nixir create mode 100644 tests/or_in_attrset.nix create mode 100644 tests/or_simple.nix create mode 100644 tests/path_concat.nix delete mode 100644 tests/precedence.nixir create mode 100644 tests/select_or_default.nix delete mode 100644 tests/simple.nixir delete mode 100644 tests/simple_op.nixir delete mode 100644 tests/unary.nixir diff --git a/tests/ancient_let.nix b/tests/ancient_let.nix new file mode 100644 index 0000000..3d4cfec --- /dev/null +++ b/tests/ancient_let.nix @@ -0,0 +1,8 @@ +# Test ancient let syntax: let { bindings; body = expr; } +# This is equivalent to: let bindings in expr, but has been deprecated +# in newer Nix versions. +let { + x = 10; + y = 20; + body = x + y; +} diff --git a/tests/attrset.nixir b/tests/attrset.nixir deleted file mode 100644 index 708f5ddd3bfd673373e97f8e7fb2703ef3e2b501..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109 zcmazD^7Lb3Kn08jU_LXDWC7y5#N1S{5)h{(wYY>8NS7t%l%_HQ84T4R0tncEI1ebB VngY=dk_4&D%*#tH0%>Jn1OQ!e3}FBO diff --git a/tests/block_comments.nix b/tests/block_comments.nix new file mode 100644 index 0000000..b5de60f --- /dev/null +++ b/tests/block_comments.nix @@ -0,0 +1,12 @@ +# Test block comments /* */ +/* This is a block comment */ +let + x = 42; /* inline block comment */ + /* Multi-line + block + comment */ + y = 100; +in +/* Comment before expression */ +x + y +/* Trailing comment */ diff --git a/tests/comparison.nixir b/tests/comparison.nixir deleted file mode 100644 index fb7b4fd62f9b8ac0f3c19b3a9c5eb7b0dcc7058f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107 zcmazD^7Lb3Kn08eAU;Tx5r`8(1Opd{00Iyv2`nN4<})xjfM_KU!7czIK>*AEsbT@~ I7{Me10PPL~4*&oF diff --git a/tests/dynamic_attrs.nix b/tests/dynamic_attrs.nix new file mode 100644 index 0000000..3c32fd5 --- /dev/null +++ b/tests/dynamic_attrs.nix @@ -0,0 +1,15 @@ +# Test dynamic attribute names +# Note: Full dynamic attrs require runtime evaluation +# For now, testing that syntax is recognized +let + key = "mykey"; +in { + # Static attribute for comparison + static = "value"; + + # Dynamic attribute name (basic string interpolation) + # "${key}" = "dynamic_value"; + + # For now, use workaround with static names + mykey = "works"; +} diff --git a/tests/home_path.nix b/tests/home_path.nix new file mode 100644 index 0000000..ccfb107 --- /dev/null +++ b/tests/home_path.nix @@ -0,0 +1,11 @@ +# Test home-relative paths +# Note: This will resolve to the actual home directory at evaluation time +let + # Example home path (will be expanded by evaluator) + config = ~/..config; + file = ~/.bashrc; +in { + # These are just path values that will be expanded + configPath = config; + filePath = file; +} diff --git a/tests/if.nixir b/tests/if.nixir deleted file mode 100644 index 4ee0f5992c9cf0477d61dbc9a4d28e10556fff9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58 fcmazD^7Lb3Kn08rAU+F-U}OZ7AOZ-$9GGeVQQrZN diff --git a/tests/indented_string.nix b/tests/indented_string.nix new file mode 100644 index 0000000..16c6026 --- /dev/null +++ b/tests/indented_string.nix @@ -0,0 +1,31 @@ +# Test indented strings (multi-line strings with '' delimiters) +let + # Simple indented string + simple = '' + Hello + World + ''; + + # Indented string with interpolation + name = "Nix"; + greeting = '' + Welcome to ${name}! + This is indented. + ''; + + # Escape sequences + escapes = '' + Literal dollar: ''$ + Literal quotes: ''' + Regular text + ''; + + # Shell script example (common use case) + script = '' + #!/bin/bash + echo "Running script" + ls -la + ''; +in { + inherit simple greeting escapes script; +} diff --git a/tests/let.nixir b/tests/let.nixir deleted file mode 100644 index cb9dd4171449cba19af43f6c2cefcba863c17532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75 rcmazD^7Lb3Kn08eAU;Tx5r`{51Opd{00I!F5-cJD<})w|Kxqa5kZuCH diff --git a/tests/list_concat.nix b/tests/list_concat.nix new file mode 100644 index 0000000..a1b09f1 --- /dev/null +++ b/tests/list_concat.nix @@ -0,0 +1,15 @@ +# Test list concatenation operator ++ +let + list1 = [1 2 3]; + list2 = [4 5 6]; + empty = []; +in { + # Basic concatenation + combined = list1 ++ list2; + + # Concatenate with empty list + with_empty = list1 ++ empty; + + # Nested concatenation + triple = [1] ++ [2] ++ [3]; +} diff --git a/tests/logical.nixir b/tests/logical.nixir deleted file mode 100644 index 010a5f558addb374be5e2b3786b015f964a46663..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149 zcmazD^7Lb3Kn08eAU;Tx5r`{TKs-hevl7f?Z~zG?fe3B^5D5Zc21pGELkJ-40OEjE S^59U(1X2M6U~SA0HUj{kF9W#% diff --git a/tests/nested_attrs.nix b/tests/nested_attrs.nix new file mode 100644 index 0000000..874d08b --- /dev/null +++ b/tests/nested_attrs.nix @@ -0,0 +1,13 @@ +# Test nested attribute paths +{ + # Simple nested path + a.b.c = 42; + + # Multiple nested paths + x.y = 1; + x.z = 2; + + # Mix of nested and non-nested + foo = "bar"; + nested.deep.value = 100; +} diff --git a/tests/operators.nixir b/tests/operators.nixir deleted file mode 100644 index f71f899c4b62baf38543d84c5810ffea0bb0c77b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109 zcmazD^7Lb3Kn08eAU;Tx5r`{51Opd{00I!F5-h?B<})xTfoLW$$shm~gD^nKz)E2j F008yh0|fv8 diff --git a/tests/or_in_attrset.nix b/tests/or_in_attrset.nix new file mode 100644 index 0000000..406149b --- /dev/null +++ b/tests/or_in_attrset.nix @@ -0,0 +1,6 @@ +# Test 'or' in attrset context +let + attrs = { a = 1; }; +in { + test = attrs.a or 999; +} diff --git a/tests/or_simple.nix b/tests/or_simple.nix new file mode 100644 index 0000000..6025a4d --- /dev/null +++ b/tests/or_simple.nix @@ -0,0 +1,4 @@ +# Simplest 'or' test +let + x = { a = 1; }; +in x.a or 2 diff --git a/tests/path_concat.nix b/tests/path_concat.nix new file mode 100644 index 0000000..682175c --- /dev/null +++ b/tests/path_concat.nix @@ -0,0 +1,13 @@ +# Test path concatenation +let + # Path + string = path + p1 = ./foo + "/bar"; + + # String + path = path + p2 = "/prefix" + ./suffix; + + # Path + path = path + p3 = ./dir + ./file; +in { + inherit p1 p2 p3; +} diff --git a/tests/precedence.nixir b/tests/precedence.nixir deleted file mode 100644 index de1b0d429d5c927e4d6568f669ede994f731b166..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 318 zcmY+9%MFAu3`FC=Y7uathOK}$xFE!dD}nxN!1DqqYssTLe@4!>PICj7*4 diff --git a/tests/unary.nixir b/tests/unary.nixir deleted file mode 100644 index 652fabc4b463b276334d7d94b5fbff4d886983e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 113 zcmazD^7Lb3Kn08eAU;Tx5r`{51Opd{00I!Fk_F5%0Fhv2%s@IXHC-7jAOI$T3c;fJ NC16oTuqY!?6aY;M1;+pY From 121803b13cabea41723fd566cf6c13156eb264f5 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 22 Feb 2026 21:49:58 +0300 Subject: [PATCH 8/8] irc: improve multi-line strings; complete list concat and dynamic attrs Signed-off-by: NotAShelf Change-Id: I64e53c68d90b62f3ca306865ceda32af6a6a6964 --- src/irc/evaluator.cpp | 72 ++++++++++++++++++++++++------ src/irc/ir_gen.cpp | 14 ++++-- src/irc/parser.cpp | 99 +++++++++++++++++++++++++++++------------- src/irc/serializer.cpp | 44 ++++++++++++++++--- src/irc/types.h | 36 ++++++++++++--- 5 files changed, 204 insertions(+), 61 deletions(-) diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cpp index 6004f01..7a50dc7 100644 --- a/src/irc/evaluator.cpp +++ b/src/irc/evaluator.cpp @@ -130,6 +130,14 @@ struct Evaluator::Impl { // We can use EvalState's searchPath to resolve auto path = state.findFile(n->value); v.mkPath(path); + } else if (auto* n = node->get_if()) { + // Evaluate list - allocate and populate + auto builder = state.buildList(n->elements.size()); + for (size_t i = 0; i < n->elements.size(); i++) { + builder.elems[i] = state.allocValue(); + eval_node(n->elements[i], *builder.elems[i], env); + } + v.mkList(builder); } else if (auto* n = node->get_if()) { Value* bound = env ? env->lookup(n->index) : nullptr; if (!bound && env && n->name.has_value()) { @@ -316,15 +324,34 @@ struct Evaluator::Impl { state.error("type error in comparison").debugThrow(); } break; - case BinaryOp::CONCAT: - // TODO: ++ list concatenation requires accessing private Nix Value payload - // For now, delegate to Nix's concatLists or implement via builtins - // Parser recognizes ++ but evaluator not yet fully implemented - state - .error( - "list concatenation (++) not yet fully implemented - use builtins.concatLists") - .debugThrow(); + case BinaryOp::CONCAT: { + // List concatenation: left ++ right + if (left->type() != nList || right->type() != nList) { + state.error("list concatenation requires two lists").debugThrow(); + } + + size_t left_size = left->listSize(); + size_t right_size = right->listSize(); + size_t total_size = left_size + right_size; + + auto builder = state.buildList(total_size); + auto left_view = left->listView(); + auto right_view = right->listView(); + + // Copy elements from left list + size_t idx = 0; + for (auto elem : left_view) { + builder.elems[idx++] = elem; + } + + // Copy elements from right list + for (auto elem : right_view) { + builder.elems[idx++] = elem; + } + + v.mkList(builder); break; + } case BinaryOp::MERGE: { // // is attrset merge - right overrides left if (left->type() != nAttrs || right->type() != nAttrs) { @@ -418,16 +445,33 @@ struct Evaluator::Impl { // 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); - attr_env->bind(thunk); + for (const auto& binding : n->attrs) { + if (!binding.is_dynamic()) { + Value* thunk = make_thunk(binding.value, attr_env); + attr_env->bind(thunk); + } } } // Attributes should be lazy, so store as thunks and not evaluated values - for (const auto& [key, val] : n->attrs) { - Value* attr_val = make_thunk(val, attr_env); - bindings.insert(state.symbols.create(key), attr_val); + for (const auto& binding : n->attrs) { + Value* attr_val = make_thunk(binding.value, attr_env); + + if (binding.is_dynamic()) { + // Evaluate key expression to get attribute name + Value* key_val = state.allocValue(); + eval_node(binding.dynamic_name, *key_val, attr_env); + force(key_val); + + if (key_val->type() != nString) { + state.error("dynamic attribute name must evaluate to a string").debugThrow(); + } + + std::string key_str = std::string(key_val->c_str()); + bindings.insert(state.symbols.create(key_str), attr_val); + } else { + bindings.insert(state.symbols.create(binding.static_name.value()), attr_val); + } } v.mkAttrs(bindings.finish()); diff --git a/src/irc/ir_gen.cpp b/src/irc/ir_gen.cpp index 95999c8..ff9f18c 100644 --- a/src/irc/ir_gen.cpp +++ b/src/irc/ir_gen.cpp @@ -122,11 +122,17 @@ struct IRGenerator::Impl { if (auto* n = node.get_if()) { AttrsetNode attrs(n->recursive, n->line); name_resolver.enter_scope(); - for (const auto& [key, val] : n->attrs) { - name_resolver.bind(key); + for (const auto& binding : n->attrs) { + if (!binding.is_dynamic()) { + name_resolver.bind(binding.static_name.value()); + } } - for (const auto& [key, val] : n->attrs) { - attrs.attrs.push_back({key, convert(val)}); + for (const auto& binding : n->attrs) { + if (binding.is_dynamic()) { + attrs.attrs.push_back(AttrBinding(convert(binding.dynamic_name), convert(binding.value))); + } else { + attrs.attrs.push_back(AttrBinding(binding.static_name.value(), convert(binding.value))); + } } name_resolver.exit_scope(); return std::make_shared(attrs); diff --git a/src/irc/parser.cpp b/src/irc/parser.cpp index 3780fb7..8d92617 100644 --- a/src/irc/parser.cpp +++ b/src/irc/parser.cpp @@ -426,7 +426,30 @@ private: pos += 3; continue; } else if (pos + 2 < input.size() && input[pos + 2] == '\\') { - // ''\ -> escape for backslash + // ''\ -> check what follows + if (pos + 3 < input.size()) { + char next = input[pos + 3]; + if (next == 'n') { + raw_content += '\n'; + pos += 4; + continue; + } else if (next == 'r') { + raw_content += '\r'; + pos += 4; + continue; + } else if (next == 't') { + raw_content += '\t'; + pos += 4; + continue; + } else if (next == ' ' || next == '\t') { + // ''\ before whitespace - preserve the whitespace (mark it specially) + raw_content += "\x01"; // Use control char as marker for preserved whitespace + raw_content += next; + pos += 4; + continue; + } + } + // Default: literal backslash raw_content += '\\'; pos += 3; continue; @@ -486,12 +509,17 @@ private: } // Find minimum indentation (spaces/tabs at start of non-empty lines) + // \x01 marker indicates preserved whitespace (from ''\ escape) size_t min_indent = std::string::npos; for (const auto& line : lines) { if (line.empty()) continue; // Skip empty lines when calculating indentation size_t indent = 0; - for (char c : line) { + for (size_t i = 0; i < line.size(); i++) { + char c = line[i]; + // If we hit the preserved whitespace marker, stop counting indentation + if (c == '\x01') + break; if (c == ' ' || c == '\t') indent++; else @@ -504,7 +532,7 @@ private: if (min_indent == std::string::npos) min_indent = 0; - // Strip min_indent from all lines + // Strip min_indent from all lines and remove \x01 markers std::string result; for (size_t i = 0; i < lines.size(); i++) { const auto& line = lines[i]; @@ -513,9 +541,25 @@ private: if (i + 1 < lines.size()) result += '\n'; } else { - // Strip indentation - size_t skip = std::min(min_indent, line.size()); - result += line.substr(skip); + // Strip indentation, being careful about \x01 markers + size_t skip = 0; + size_t pos = 0; + while (skip < min_indent && pos < line.size()) { + if (line[pos] == '\x01') { + // Hit preserved whitespace marker - don't strip any more + break; + } + skip++; + pos++; + } + + // Add the rest of the line, removing \x01 markers + for (size_t j = pos; j < line.size(); j++) { + if (line[j] != '\x01') { + result += line[j]; + } + } + if (i + 1 < lines.size()) result += '\n'; } @@ -1025,11 +1069,11 @@ public: // inherit (expr) x → x = expr.x auto select = std::make_shared( SelectNode(source, std::make_shared(ConstStringNode(name.value)))); - attrs.attrs.push_back({name.value, select}); + attrs.attrs.push_back(AttrBinding(name.value, select)); } else { // inherit x → x = x auto var = std::make_shared(VarNode(0, name.value)); - attrs.attrs.push_back({name.value, var}); + attrs.attrs.push_back(AttrBinding(name.value, var)); } } @@ -1046,11 +1090,8 @@ public: if (consume(Token::EQUALS)) { auto value = parse_expr(); - // For dynamic attrs, we use special marker in key and store expr as value - // This will need runtime evaluation - store as special node - // For now, convert to string at parse time if possible - // TODO: Full dynamic attr support needs IR node for dynamic keys - attrs.attrs.push_back({"__dynamic__", value}); + // Dynamic attribute - name is evaluated at runtime + attrs.attrs.push_back(AttrBinding(name_expr, value)); } } else if (current().type == Token::IDENT || current().type == Token::STRING || current().type == Token::INDENTED_STRING) { @@ -1076,22 +1117,22 @@ public: // Desugar nested paths: a.b.c = v becomes a = { b = { c = v; }; } if (path.size() == 1) { // Simple case: just one key - attrs.attrs.push_back({path[0], value}); + attrs.attrs.push_back(AttrBinding(path[0], value)); } else { // Nested case: build nested attrsets from right to left auto nested = value; for (int i = path.size() - 1; i > 0; i--) { auto inner_attrs = AttrsetNode(false); - inner_attrs.attrs.push_back({path[i], nested}); + inner_attrs.attrs.push_back(AttrBinding(path[i], nested)); nested = std::make_shared(std::move(inner_attrs)); } - attrs.attrs.push_back({path[0], nested}); + attrs.attrs.push_back(AttrBinding(path[0], nested)); } } else if (consume(Token::AT)) { // @ pattern - not affected by nested paths auto pattern = parse_expr(); auto value = parse_expr(); - attrs.attrs.push_back({path[0], value}); + attrs.attrs.push_back(AttrBinding(path[0], value)); } } @@ -1111,27 +1152,25 @@ public: } std::shared_ptr parse_list() { - std::shared_ptr list = std::make_shared(ConstNullNode()); + std::vector> elements; if (consume(Token::RBRACKET)) { - return list; + return std::make_shared(ListNode(elements)); } - std::vector> elements; while (current().type != Token::RBRACKET) { elements.push_back(parse_expr()); - if (!consume(Token::COMMA)) - break; - } - expect(Token::RBRACKET); - - for (auto it = elements.rbegin(); it != elements.rend(); ++it) { - list = std::make_shared(AppNode( - std::make_shared(AppNode(std::make_shared(VarNode(0, "__list")), *it)), - list)); + if (!consume(Token::RBRACKET)) { + // Elements are whitespace-separated in Nix, no comma required + // But we'll continue parsing until we hit ] + } else { + // Found closing bracket + return std::make_shared(ListNode(elements)); + } } - return list; + // Unreachable, but for safety + return std::make_shared(ListNode(elements)); } void parse_bindings(std::vector>>& bindings) { diff --git a/src/irc/serializer.cpp b/src/irc/serializer.cpp index e1c3962..f5b1982 100644 --- a/src/irc/serializer.cpp +++ b/src/irc/serializer.cpp @@ -64,6 +64,8 @@ struct Serializer::Impl { return NodeType::HAS_ATTR; if (node.holds()) return NodeType::WITH; + if (node.holds()) + return NodeType::LIST; if (node.holds()) return NodeType::IF; if (node.holds()) @@ -129,10 +131,16 @@ struct Serializer::Impl { } else if (auto* n = node.get_if()) { write_u8(n->recursive ? 1 : 0); write_u32(n->attrs.size()); - for (const auto& [key, val] : n->attrs) { - write_string(key); - if (val) - write_node(*val); + for (const auto& binding : n->attrs) { + if (binding.is_dynamic()) { + write_u8(1); // Dynamic flag + write_node(*binding.dynamic_name); + } else { + write_u8(0); // Static flag + write_string(binding.static_name.value()); + } + if (binding.value) + write_node(*binding.value); } } else if (auto* n = node.get_if()) { if (n->expr) @@ -155,6 +163,12 @@ struct Serializer::Impl { write_node(*n->attrs); if (n->body) write_node(*n->body); + } else if (auto* n = node.get_if()) { + write_u32(n->elements.size()); + for (const auto& elem : n->elements) { + if (elem) + write_node(*elem); + } } else if (auto* n = node.get_if()) { if (n->cond) write_node(*n->cond); @@ -335,9 +349,16 @@ struct Deserializer::Impl { uint32_t num_attrs = read_u32(); AttrsetNode attrs(recursive, line); for (uint32_t i = 0; i < num_attrs; i++) { - std::string key = read_string(); - auto val = read_node(); - attrs.attrs.push_back({key, val}); + uint8_t is_dynamic = read_u8(); + if (is_dynamic) { + auto key_expr = read_node(); + auto val = read_node(); + attrs.attrs.push_back(AttrBinding(key_expr, val)); + } else { + std::string key = read_string(); + auto val = read_node(); + attrs.attrs.push_back(AttrBinding(key, val)); + } } return std::make_shared(std::move(attrs)); } @@ -363,6 +384,15 @@ struct Deserializer::Impl { auto body = read_node(); return std::make_shared(WithNode(attrs, body, line)); } + case NodeType::LIST: { + uint32_t num_elements = read_u32(); + std::vector> elements; + elements.reserve(num_elements); +for (uint32_t i = 0; i < num_elements; i++) { + elements.push_back(read_node()); + } + return std::make_shared(ListNode(std::move(elements), line)); + } case NodeType::IF: { auto cond = read_node(); auto then_branch = read_node(); diff --git a/src/irc/types.h b/src/irc/types.h index a777a8c..7b0765d 100644 --- a/src/irc/types.h +++ b/src/irc/types.h @@ -34,6 +34,7 @@ enum class NodeType : uint8_t { SELECT = 0x31, HAS_ATTR = 0x34, WITH = 0x32, + LIST = 0x33, IF = 0x40, LET = 0x50, LETREC = 0x51, @@ -152,8 +153,24 @@ struct UnaryOpNode { UnaryOpNode(UnaryOp o, std::shared_ptr operand, uint32_t l = 0); }; +struct AttrBinding { + std::optional static_name; // Static key like "foo" + std::shared_ptr dynamic_name; // Dynamic key like ${expr} + std::shared_ptr value; + + // Static attribute + AttrBinding(std::string name, std::shared_ptr val) + : static_name(std::move(name)), value(std::move(val)) {} + + // Dynamic attribute + AttrBinding(std::shared_ptr name_expr, std::shared_ptr val) + : dynamic_name(std::move(name_expr)), value(std::move(val)) {} + + bool is_dynamic() const { return !static_name.has_value(); } +}; + struct AttrsetNode { - std::vector>> attrs; + std::vector attrs; bool recursive = false; uint32_t line = 0; AttrsetNode(bool rec = false, uint32_t l = 0) : recursive(rec), line(l) {} @@ -228,14 +245,21 @@ struct ForceNode { ForceNode(std::shared_ptr e, uint32_t l = 0); }; +struct ListNode { + std::vector> elements; + uint32_t line = 0; + ListNode(std::vector> elems = {}, uint32_t l = 0) + : elements(std::move(elems)), line(l) {} +}; + // Node wraps a variant for type-safe AST class Node { public: - using Variant = - std::variant; + using Variant = std::variant; Variant data;