diff --git a/src/irc/evaluator.cpp b/src/irc/evaluator.cpp index dfd6eb1..7a50dc7 100644 --- a/src/irc/evaluator.cpp +++ b/src/irc/evaluator.cpp @@ -5,9 +5,7 @@ #include "evaluator.h" #include "nix/expr/eval.hh" #include "nix/expr/value.hh" -#include "nix/util/error.hh" - -#include +#include "nix/util/url.hh" #include namespace nix_irc { @@ -66,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); @@ -108,14 +102,42 @@ 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()) { - 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) + } 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 - 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()) { + // 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()) { @@ -216,6 +238,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(); } @@ -286,10 +324,60 @@ struct Evaluator::Impl { state.error("type error in comparison").debugThrow(); } break; - case BinaryOp::CONCAT: - // ++ is list concatenation in Nix; string concat uses ADD (+) - state.error("list concatenation not yet implemented").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) { + 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(); } @@ -335,17 +423,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); } @@ -355,21 +442,36 @@ 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); - 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); + } } } - for (const auto& [key, val] : n->attrs) { - Value* attr_val = state.allocValue(); - if (n->recursive) { - eval_node(val, *attr_val, attr_env); + // Attributes should be lazy, so store as thunks and not evaluated values + 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 { - eval_node(val, *attr_val, env); + bindings.insert(state.symbols.create(binding.static_name.value()), attr_val); } - bindings.insert(state.symbols.create(key), attr_val); } v.mkAttrs(bindings.finish()); @@ -446,6 +548,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/ir_gen.cpp b/src/irc/ir_gen.cpp index a2561be..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); @@ -162,6 +168,7 @@ struct IRGenerator::Impl { name_resolver.bind(key); } std::vector>> new_bindings; + new_bindings.reserve(n->bindings.size()); for (const auto& [key, val] : n->bindings) { new_bindings.push_back({key, convert(val)}); } @@ -177,6 +184,7 @@ struct IRGenerator::Impl { name_resolver.bind(key); } std::vector>> new_bindings; + new_bindings.reserve(n->bindings.size()); for (const auto& [key, val] : n->bindings) { new_bindings.push_back({key, convert(val)}); } diff --git a/src/irc/parser.cpp b/src/irc/parser.cpp index e72d034..8d92617 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 { @@ -67,8 +70,13 @@ struct Token { IDENT, STRING, STRING_INTERP, + INDENTED_STRING, + INDENTED_STRING_INTERP, PATH, + LOOKUP_PATH, INT, + FLOAT, + URI, BOOL, LET, IN, @@ -79,6 +87,7 @@ struct Token { ASSERT, WITH, INHERIT, + IMPORT, DOT, SEMICOLON, COLON, @@ -93,6 +102,7 @@ struct Token { STAR, SLASH, CONCAT, + MERGE, EQEQ, NE, LT, @@ -145,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(); } @@ -171,6 +183,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; @@ -197,7 +213,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 == '!') { @@ -213,17 +251,48 @@ 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])) { - 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++; @@ -242,7 +311,7 @@ private: size_t line; size_t col; - void emit(Token t) { + void emit(const Token& t) { tokens.push_back(t); pos++; col++; @@ -260,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; } @@ -317,10 +404,175 @@ 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] == '\\') { + // ''\ -> 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; + } 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) + // \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 (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 + break; + } + if (indent < min_indent) + min_indent = indent; + } + + if (min_indent == std::string::npos) + min_indent = 0; + + // 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]; + if (line.empty()) { + // Preserve empty lines + if (i + 1 < lines.size()) + result += '\n'; + } else { + // 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'; + } + } + + 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); @@ -328,6 +580,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] == '-') @@ -339,12 +607,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; @@ -364,6 +668,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") @@ -410,6 +716,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: @@ -450,6 +758,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: @@ -488,6 +798,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); @@ -550,42 +899,30 @@ 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 + // 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; } - void parse_expr_attrs(std::shared_ptr&) { - // Extended selection syntax - } - std::shared_ptr parse_expr2() { std::shared_ptr left = parse_expr3(); @@ -609,6 +946,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(); @@ -646,6 +989,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)); @@ -657,11 +1010,27 @@ 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)); } + 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")); @@ -700,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)); } } @@ -712,18 +1081,58 @@ 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}); + // 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) { + // 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(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(AttrBinding(path[i], nested)); + nested = std::make_shared(std::move(inner_attrs)); + } + 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({key_str, value}); + attrs.attrs.push_back(AttrBinding(path[0], value)); } } @@ -743,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 fff2208..f5b1982 100644 --- a/src/irc/serializer.cpp +++ b/src/irc/serializer.cpp @@ -1,7 +1,6 @@ #include "serializer.h" #include #include -#include namespace nix_irc { @@ -31,6 +30,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 +40,10 @@ 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::CONST_LOOKUP_PATH; if (node.holds()) return NodeType::VAR; if (node.holds()) @@ -49,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()) @@ -57,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()) @@ -78,6 +87,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 +100,10 @@ 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_string(n->value); } else if (auto* n = node.get_if()) { write_u32(n->index); } else if (auto* n = node.get_if()) { @@ -107,13 +125,22 @@ 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()); - 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) @@ -136,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); @@ -254,6 +287,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 +307,14 @@ 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::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)); @@ -293,14 +340,25 @@ 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(); 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)); } @@ -326,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 f52db8d..7b0765d 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 @@ -19,19 +17,24 @@ 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, + 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, WITH = 0x32, + LIST = 0x33, IF = 0x40, LET = 0x50, LETREC = 0x51, @@ -41,7 +44,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 }; @@ -77,6 +96,24 @@ 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 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; @@ -116,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) {} @@ -174,6 +227,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; @@ -186,13 +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; @@ -239,6 +306,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) {} 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 708f5dd..0000000 Binary files a/tests/attrset.nixir and /dev/null differ 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 fb7b4fd..0000000 Binary files a/tests/comparison.nixir and /dev/null differ 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/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/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 4ee0f59..0000000 Binary files a/tests/if.nixir and /dev/null differ 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/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 cb9dd41..0000000 Binary files a/tests/let.nixir and /dev/null differ 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 010a5f5..0000000 Binary files a/tests/logical.nixir and /dev/null differ 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/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/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 f71f899..0000000 Binary files a/tests/operators.nixir and /dev/null differ 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 de1b0d4..0000000 Binary files a/tests/precedence.nixir and /dev/null differ 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/select_or_default.nix b/tests/select_or_default.nix new file mode 100644 index 0000000..df91875 --- /dev/null +++ b/tests/select_or_default.nix @@ -0,0 +1,16 @@ +# Test selection with 'or' default +let + attrs = { a = 1; b = 2; }; +in { + # Attribute exists - should use value from attrs + has_attr = attrs.a or 999; + + # Attribute doesn't exist - should use default + missing_attr = attrs.c or 100; + + # Nested default expression + nested = attrs.d or (attrs.a + attrs.b); + + # Default with literal + with_string = attrs.name or "default_name"; +} diff --git a/tests/simple.nixir b/tests/simple.nixir deleted file mode 100644 index 3e26f83..0000000 Binary files a/tests/simple.nixir and /dev/null differ diff --git a/tests/simple_op.nixir b/tests/simple_op.nixir deleted file mode 100644 index 18ffbd3..0000000 Binary files a/tests/simple_op.nixir and /dev/null differ 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"; } diff --git a/tests/unary.nixir b/tests/unary.nixir deleted file mode 100644 index 652fabc..0000000 Binary files a/tests/unary.nixir and /dev/null differ diff --git a/tests/uri_test.nix b/tests/uri_test.nix new file mode 100644 index 0000000..ad93389 --- /dev/null +++ b/tests/uri_test.nix @@ -0,0 +1,3 @@ +https://example.com/path?query=1 +#frag +