Compare commits

...

8 commits

Author SHA1 Message Date
121803b13c
irc: improve multi-line strings; complete list concat and dynamic attrs
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I64e53c68d90b62f3ca306865ceda32af6a6a6964
2026-02-22 23:19:43 +03:00
00a3d2e585
tests: update test cases for newer syntax items; drop old artifacts
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I8640148e8e7597924f9c776750c856266a6a6964
2026-02-22 23:19:42 +03:00
ed8f637c99
irc: more syntax support
Indented strings, ancient let bindings and a bit more

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ib86c2d8ca4402dfa0c5c536a9959f4006a6a6964
2026-02-22 23:19:41 +03:00
77aa67c7e0
tests: add tests for lookup paths and imports
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I7e54691aa3e81efcb495124d13e8c24a6a6a6964
2026-02-22 23:19:40 +03:00
a6aade6c11
irc: support lookup paths and import keyword
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0d16726646aef82ce675c4f8d209029a6a6a6964
2026-02-22 23:19:39 +03:00
3c1ce0fd31
tests: add test fixture for merge operator
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ie8d8e5fb817349469fed194773120ce86a6a6964
2026-02-22 23:19:38 +03:00
59fcd3ef92
irc: support merge operator
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Icfb0cc81542e637d4b91c6a5788370fb6a6a6964
2026-02-22 23:19:37 +03:00
38c13de01d
irc: add Float and URI literal support
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I40c59d94f650e7b9e68f77598492d7ab6a6a6964
2026-02-22 23:19:36 +03:00
35 changed files with 1083 additions and 116 deletions

View file

@ -5,9 +5,7 @@
#include "evaluator.h"
#include "nix/expr/eval.hh"
#include "nix/expr/value.hh"
#include "nix/util/error.hh"
#include <stdexcept>
#include "nix/util/url.hh"
#include <unordered_map>
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<ConstIntNode>()) {
v.mkInt(n->value);
} else if (auto* n = node->get_if<ConstFloatNode>()) {
v.mkFloat(n->value);
} else if (auto* n = node->get_if<ConstStringNode>()) {
v.mkString(n->value);
} else if (auto* n = node->get_if<ConstPathNode>()) {
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<ConstBoolNode>()) {
v.mkBool(n->value);
} else if (auto* n = node->get_if<ConstNullNode>()) { // NOLINT(bugprone-branch-clone)
} else if (auto* n = node->get_if<ConstNullNode>()) { // NOLINT(bugprone-branch-clone)
v.mkNull();
} else if (auto* n = node->get_if<ConstURINode>()) {
// Parse and validate URI, then create string with URI context
auto parsed = parseURL(n->value, true);
// Store URI with context - use simple mkString with context
v.mkString(parsed.to_string(), nix::NixStringContext{});
} else if (auto* n = node->get_if<ConstLookupPathNode>()) {
// Lookup path like <nixpkgs>; resolve via Nix search path
// We can use EvalState's searchPath to resolve
auto path = state.findFile(n->value);
v.mkPath(path);
} else if (auto* n = node->get_if<ListNode>()) {
// 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<VarNode>()) {
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<EvalError>("type error in addition").debugThrow();
}
@ -286,10 +324,60 @@ struct Evaluator::Impl {
state.error<EvalError>("type error in comparison").debugThrow();
}
break;
case BinaryOp::CONCAT:
// ++ is list concatenation in Nix; string concat uses ADD (+)
state.error<EvalError>("list concatenation not yet implemented").debugThrow();
case BinaryOp::CONCAT: {
// List concatenation: left ++ right
if (left->type() != nList || right->type() != nList) {
state.error<EvalError>("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<EvalError>("attrset merge requires two attrsets").debugThrow();
}
// Build a map of right attrs first (these have priority)
std::unordered_map<Symbol, Value*> right_attrs;
for (auto& attr : *right->attrs()) {
right_attrs[attr.name] = attr.value;
}
// Copy right attrs to result
auto builder = state.buildBindings(left->attrs()->size() + right->attrs()->size());
for (auto& attr : *right->attrs()) {
builder.insert(attr.name, attr.value);
}
// Add left attrs that don't exist in right
for (auto& attr : *left->attrs()) {
if (right_attrs.find(attr.name) == right_attrs.end()) {
builder.insert(attr.name, attr.value);
}
}
v.mkAttrs(builder.finish());
break;
}
default:
state.error<EvalError>("unknown binary operator").debugThrow();
}
@ -335,17 +423,16 @@ struct Evaluator::Impl {
} else if (auto* n = node->get_if<LetNode>()) {
auto let_env = make_env(env);
for (const auto& [name, expr] : n->bindings) {
Value* val = make_thunk(expr, env);
// Create thunks in let_env so bindings can reference each other
Value* val = make_thunk(expr, let_env);
let_env->bind(val);
}
eval_node(n->body, v, let_env);
} else if (auto* n = node->get_if<LetRecNode>()) {
auto letrec_env = make_env(env);
std::vector<Value*> thunk_vals;
for (const auto& [name, expr] : n->bindings) {
Value* val = make_thunk(expr, letrec_env);
thunk_vals.push_back(val);
letrec_env->bind(val);
}
@ -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<EvalError>("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<ImportNode>()) {
// Evaluate path expression to get the file path
Value* path_val = state.allocValue();
eval_node(n->path, *path_val, env);
force(path_val);
// Path should be a string or path type, convert to SourcePath
if (path_val->type() == nPath) {
state.evalFile(path_val->path(), v);
} else if (path_val->type() == nString) {
auto path = state.rootPath(CanonPath(path_val->c_str()));
state.evalFile(path, v);
} else {
state.error<EvalError>("import argument must be a path or string").debugThrow();
}
} else {
v.mkNull();
}

View file

@ -122,11 +122,17 @@ struct IRGenerator::Impl {
if (auto* n = node.get_if<AttrsetNode>()) {
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<Node>(attrs);
@ -162,6 +168,7 @@ struct IRGenerator::Impl {
name_resolver.bind(key);
}
std::vector<std::pair<std::string, std::shared_ptr<Node>>> 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<std::pair<std::string, std::shared_ptr<Node>>> new_bindings;
new_bindings.reserve(n->bindings.size());
for (const auto& [key, val] : n->bindings) {
new_bindings.push_back({key, convert(val)});
}

View file

@ -4,8 +4,6 @@
#include <cstdlib>
#include <iostream>
#include <memory>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <vector>
@ -24,22 +22,27 @@ static std::string read_file(const std::string& path) {
if (!f) {
throw std::runtime_error("Cannot open file: " + path);
}
// Ensure FILE* is always closed
auto file_closer = [](FILE* fp) {
if (fp)
fclose(fp);
};
std::unique_ptr<FILE, decltype(file_closer)> file_guard(f, file_closer);
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
std::string content(size, '\0');
if (fread(content.data(), 1, size, f) != static_cast<size_t>(size)) {
fclose(f);
throw std::runtime_error("Failed to read file: " + path);
}
fclose(f);
return content;
}
static std::pair<std::string, std::string> run_command(const std::string& cmd) {
std::array<char, 256> buffer;
std::string result;
std::string error;
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe)
@ -53,7 +56,7 @@ static std::pair<std::string, std::string> run_command(const std::string& cmd) {
if (status != 0) {
throw std::runtime_error("Command failed: " + cmd);
}
return {result, error};
return {result, ""};
}
struct Token {
@ -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 <nixpkgs> vs comparison operator
size_t end = pos + 1;
bool is_lookup_path = false;
// Scan for valid lookup path characters until >
while (end < input.size() &&
(isalnum(input[end]) || input[end] == '-' || input[end] == '_' ||
input[end] == '/' || input[end] == '.')) {
end++;
}
// If we found > and there's content, it's a lookup path
if (end < input.size() && input[end] == '>' && end > pos + 1) {
std::string path = input.substr(pos + 1, end - pos - 1);
tokens.push_back({Token::LOOKUP_PATH, path, line, col});
pos = end + 1;
col += (end - pos + 1);
is_lookup_path = true;
}
if (!is_lookup_path) {
emit(TOKEN(LT));
}
} else if (c == '>') {
emit(TOKEN(GT));
} else if (c == '!') {
@ -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<std::string> 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<Node>(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<std::pair<std::string, std::shared_ptr<Node>>> bindings;
std::shared_ptr<Node> 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<Node>(std::move(letrec));
}
// Modern let syntax: let x = 1; in x
bool is_rec = consume(Token::REC);
std::vector<std::pair<std::string, std::shared_ptr<Node>>> bindings;
parse_bindings(bindings);
@ -550,42 +899,30 @@ public:
if (name.type == Token::IDENT) {
advance();
auto attr = std::make_shared<Node>(ConstStringNode(name.value));
auto result = std::make_shared<Node>(SelectNode(left, attr));
if (consume(Token::DOT)) {
Token name2 = current();
if (name2.type == Token::IDENT) {
advance();
auto attr2 = std::make_shared<Node>(ConstStringNode(name2.value));
auto* curr = result->get_if<SelectNode>();
while (curr && consume(Token::DOT)) {
Token n = current();
expect(Token::IDENT);
auto a = std::make_shared<Node>(ConstStringNode(n.value));
curr->attr =
std::make_shared<Node>(AppNode(std::make_shared<Node>(AppNode(curr->attr, a)),
std::make_shared<Node>(ConstNullNode())));
}
}
}
return result;
} else if (consume(Token::LBRACE)) {
auto result = std::make_shared<Node>(
SelectNode(left, std::make_shared<Node>(ConstStringNode(name.value))));
parse_expr_attrs(result);
expect(Token::RBRACE);
return result;
left = std::make_shared<Node>(SelectNode(left, attr));
// Continue loop to handle multi-dot selections (a.b.c)
continue;
}
return left;
// If we get here, the token after DOT was not IDENT
// 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<SelectNode>() && 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<SelectNode>();
select->default_expr = default_expr;
}
return left;
}
void parse_expr_attrs(std::shared_ptr<Node>&) {
// Extended selection syntax
}
std::shared_ptr<Node> parse_expr2() {
std::shared_ptr<Node> left = parse_expr3();
@ -609,6 +946,12 @@ public:
}
std::shared_ptr<Node> parse_expr3() {
// Handle import expression
if (consume(Token::IMPORT)) {
auto path_expr = parse_expr3();
return std::make_shared<Node>(ImportNode(path_expr));
}
// Handle unary operators
if (consume(Token::MINUS)) {
auto operand = parse_expr3();
@ -646,6 +989,16 @@ public:
return std::make_shared<Node>(ConstIntNode(std::stoll(t.value)));
}
if (t.type == Token::FLOAT) {
advance();
return std::make_shared<Node>(ConstFloatNode(std::stod(t.value)));
}
if (t.type == Token::URI) {
advance();
return std::make_shared<Node>(ConstURINode(t.value));
}
if (t.type == Token::STRING) {
advance();
return std::make_shared<Node>(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<Node>(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<Node>(ConstPathNode(t.value));
}
if (t.type == Token::LOOKUP_PATH) {
advance();
return std::make_shared<Node>(ConstLookupPathNode(t.value));
}
if (t.type == Token::BOOL) {
advance();
return std::make_shared<Node>(ConstBoolNode(t.value == "true"));
@ -700,11 +1069,11 @@ public:
// inherit (expr) x → x = expr.x
auto select = std::make_shared<Node>(
SelectNode(source, std::make_shared<Node>(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<Node>(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<std::string> 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<Node>(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<Node> parse_list() {
std::shared_ptr<Node> list = std::make_shared<Node>(ConstNullNode());
std::vector<std::shared_ptr<Node>> elements;
if (consume(Token::RBRACKET)) {
return list;
return std::make_shared<Node>(ListNode(elements));
}
std::vector<std::shared_ptr<Node>> 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<Node>(AppNode(
std::make_shared<Node>(AppNode(std::make_shared<Node>(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<Node>(ListNode(elements));
}
}
return list;
// Unreachable, but for safety
return std::make_shared<Node>(ListNode(elements));
}
void parse_bindings(std::vector<std::pair<std::string, std::shared_ptr<Node>>>& bindings) {

View file

@ -1,7 +1,6 @@
#include "serializer.h"
#include <cstring>
#include <iostream>
#include <sstream>
namespace nix_irc {
@ -31,6 +30,8 @@ struct Serializer::Impl {
NodeType get_node_type(const Node& node) {
if (node.holds<ConstIntNode>())
return NodeType::CONST_INT;
if (node.holds<ConstFloatNode>())
return NodeType::CONST_FLOAT;
if (node.holds<ConstStringNode>())
return NodeType::CONST_STRING;
if (node.holds<ConstPathNode>())
@ -39,6 +40,10 @@ struct Serializer::Impl {
return NodeType::CONST_BOOL;
if (node.holds<ConstNullNode>())
return NodeType::CONST_NULL;
if (node.holds<ConstURINode>())
return NodeType::CONST_URI;
if (node.holds<ConstLookupPathNode>())
return NodeType::CONST_LOOKUP_PATH;
if (node.holds<VarNode>())
return NodeType::VAR;
if (node.holds<LambdaNode>())
@ -49,6 +54,8 @@ struct Serializer::Impl {
return NodeType::BINARY_OP;
if (node.holds<UnaryOpNode>())
return NodeType::UNARY_OP;
if (node.holds<ImportNode>())
return NodeType::IMPORT;
if (node.holds<AttrsetNode>())
return NodeType::ATTRSET;
if (node.holds<SelectNode>())
@ -57,6 +64,8 @@ struct Serializer::Impl {
return NodeType::HAS_ATTR;
if (node.holds<WithNode>())
return NodeType::WITH;
if (node.holds<ListNode>())
return NodeType::LIST;
if (node.holds<IfNode>())
return NodeType::IF;
if (node.holds<LetNode>())
@ -78,6 +87,11 @@ struct Serializer::Impl {
if (auto* n = node.get_if<ConstIntNode>()) {
write_u64(static_cast<uint64_t>(n->value));
} else if (auto* n = node.get_if<ConstFloatNode>()) {
double val = n->value;
uint64_t bits = 0;
std::memcpy(&bits, &val, sizeof(bits));
write_u64(bits);
} else if (auto* n = node.get_if<ConstStringNode>()) {
write_string(n->value);
} else if (auto* n = node.get_if<ConstPathNode>()) {
@ -86,6 +100,10 @@ struct Serializer::Impl {
write_u8(n->value ? 1 : 0);
} else if (auto* n = node.get_if<ConstNullNode>()) {
// No data for null
} else if (auto* n = node.get_if<ConstURINode>()) {
write_string(n->value);
} else if (auto* n = node.get_if<ConstLookupPathNode>()) {
write_string(n->value);
} else if (auto* n = node.get_if<VarNode>()) {
write_u32(n->index);
} else if (auto* n = node.get_if<LambdaNode>()) {
@ -107,13 +125,22 @@ struct Serializer::Impl {
write_u8(static_cast<uint8_t>(n->op));
if (n->operand)
write_node(*n->operand);
} else if (auto* n = node.get_if<ImportNode>()) {
if (n->path)
write_node(*n->path);
} else if (auto* n = node.get_if<AttrsetNode>()) {
write_u8(n->recursive ? 1 : 0);
write_u32(n->attrs.size());
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<SelectNode>()) {
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<ListNode>()) {
write_u32(n->elements.size());
for (const auto& elem : n->elements) {
if (elem)
write_node(*elem);
}
} else if (auto* n = node.get_if<IfNode>()) {
if (n->cond)
write_node(*n->cond);
@ -254,6 +287,12 @@ struct Deserializer::Impl {
int64_t val = static_cast<int64_t>(read_u64());
return std::make_shared<Node>(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<Node>(ConstFloatNode(val, line));
}
case NodeType::CONST_STRING: {
std::string val = read_string();
return std::make_shared<Node>(ConstStringNode(val, line));
@ -268,6 +307,14 @@ struct Deserializer::Impl {
}
case NodeType::CONST_NULL:
return std::make_shared<Node>(ConstNullNode(line));
case NodeType::CONST_URI: {
std::string val = read_string();
return std::make_shared<Node>(ConstURINode(val, line));
}
case NodeType::CONST_LOOKUP_PATH: {
std::string val = read_string();
return std::make_shared<Node>(ConstLookupPathNode(val, line));
}
case NodeType::VAR: {
uint32_t index = read_u32();
return std::make_shared<Node>(VarNode(index, "", line));
@ -293,14 +340,25 @@ struct Deserializer::Impl {
auto operand = read_node();
return std::make_shared<Node>(UnaryOpNode(op, operand, line));
}
case NodeType::IMPORT: {
auto path = read_node();
return std::make_shared<Node>(ImportNode(path, line));
}
case NodeType::ATTRSET: {
bool recursive = read_u8() != 0;
uint32_t num_attrs = read_u32();
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<Node>(std::move(attrs));
}
@ -326,6 +384,15 @@ struct Deserializer::Impl {
auto body = read_node();
return std::make_shared<Node>(WithNode(attrs, body, line));
}
case NodeType::LIST: {
uint32_t num_elements = read_u32();
std::vector<std::shared_ptr<Node>> elements;
elements.reserve(num_elements);
for (uint32_t i = 0; i < num_elements; i++) {
elements.push_back(read_node());
}
return std::make_shared<Node>(ListNode(std::move(elements), line));
}
case NodeType::IF: {
auto cond = read_node();
auto then_branch = read_node();

View file

@ -2,10 +2,8 @@
#define NIX_IRC_TYPES_H
#include <cstdint>
#include <fstream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <unordered_map>
#include <utility>
@ -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<std::string> name;
@ -116,8 +153,24 @@ struct UnaryOpNode {
UnaryOpNode(UnaryOp o, std::shared_ptr<Node> operand, uint32_t l = 0);
};
struct AttrBinding {
std::optional<std::string> static_name; // Static key like "foo"
std::shared_ptr<Node> dynamic_name; // Dynamic key like ${expr}
std::shared_ptr<Node> value;
// Static attribute
AttrBinding(std::string name, std::shared_ptr<Node> val)
: static_name(std::move(name)), value(std::move(val)) {}
// Dynamic attribute
AttrBinding(std::shared_ptr<Node> name_expr, std::shared_ptr<Node> val)
: dynamic_name(std::move(name_expr)), value(std::move(val)) {}
bool is_dynamic() const { return !static_name.has_value(); }
};
struct AttrsetNode {
std::vector<std::pair<std::string, std::shared_ptr<Node>>> attrs;
std::vector<AttrBinding> 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<Node> c, std::shared_ptr<Node> b, uint32_t l = 0);
};
struct ImportNode {
std::shared_ptr<Node> path; // Path expression to import
uint32_t line = 0;
ImportNode(std::shared_ptr<Node> p, uint32_t l = 0);
};
struct ThunkNode {
std::shared_ptr<Node> expr;
uint32_t line = 0;
@ -186,13 +245,21 @@ struct ForceNode {
ForceNode(std::shared_ptr<Node> e, uint32_t l = 0);
};
struct ListNode {
std::vector<std::shared_ptr<Node>> elements;
uint32_t line = 0;
ListNode(std::vector<std::shared_ptr<Node>> 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<ConstIntNode, ConstStringNode, ConstPathNode, ConstBoolNode,
ConstNullNode, VarNode, LambdaNode, AppNode, BinaryOpNode,
UnaryOpNode, AttrsetNode, SelectNode, HasAttrNode, WithNode, IfNode,
LetNode, LetRecNode, AssertNode, ThunkNode, ForceNode>;
using Variant = std::variant<ConstIntNode, ConstFloatNode, ConstStringNode, ConstPathNode,
ConstBoolNode, ConstNullNode, ConstURINode, ConstLookupPathNode,
VarNode, LambdaNode, AppNode, BinaryOpNode, UnaryOpNode, ImportNode,
AttrsetNode, SelectNode, HasAttrNode, WithNode, IfNode, LetNode,
LetRecNode, AssertNode, ThunkNode, ForceNode, ListNode>;
Variant data;
@ -239,6 +306,8 @@ inline LetRecNode::LetRecNode(std::shared_ptr<Node> b, uint32_t l) : body(std::m
inline AssertNode::AssertNode(std::shared_ptr<Node> c, std::shared_ptr<Node> b, uint32_t l)
: cond(std::move(c)), body(std::move(b)), line(l) {}
inline ImportNode::ImportNode(std::shared_ptr<Node> p, uint32_t l) : path(std::move(p)), line(l) {}
inline ThunkNode::ThunkNode(std::shared_ptr<Node> e, uint32_t l) : expr(std::move(e)), line(l) {}
inline ForceNode::ForceNode(std::shared_ptr<Node> e, uint32_t l) : expr(std::move(e)), line(l) {}

8
tests/ancient_let.nix Normal file
View file

@ -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;
}

Binary file not shown.

12
tests/block_comments.nix Normal file
View file

@ -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 */

Binary file not shown.

15
tests/dynamic_attrs.nix Normal file
View file

@ -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";
}

1
tests/float_test.nix Normal file
View file

@ -0,0 +1 @@
1.5

11
tests/home_path.nix Normal file
View file

@ -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;
}

Binary file not shown.

3
tests/import_lookup.nix Normal file
View file

@ -0,0 +1,3 @@
# Test import with lookup path
# Common pattern: import <nixpkgs> { }
import <nixpkgs>

11
tests/import_simple.nix Normal file
View file

@ -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 <nixpkgs> { }
# Import with path expressions:
# import (./dir + "/file.nix")

31
tests/indented_string.nix Normal file
View file

@ -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;
}

Binary file not shown.

15
tests/list_concat.nix Normal file
View file

@ -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];
}

Binary file not shown.

9
tests/lookup_path.nix Normal file
View file

@ -0,0 +1,9 @@
# Test lookup path syntax
# Lookup paths resolve via NIX_PATH environment variable
# Example: <nixpkgs> -> /nix/var/nix/profiles/per-user/root/channels/nixpkgs
# Simple lookup path
<nixpkgs>
# Nested lookup path (common pattern)
# <nixpkgs/lib>

View file

@ -0,0 +1,3 @@
# Test nested lookup path
# Common pattern in Nix: <nixpkgs/lib> or <nixpkgs/pkgs/stdenv>
<nixpkgs/lib>

2
tests/merge.nix Normal file
View file

@ -0,0 +1,2 @@
# Test attrset merge operator (//)
{a = {x = 1;} // {y = 2;};}

13
tests/nested_attrs.nix Normal file
View file

@ -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;
}

Binary file not shown.

6
tests/or_in_attrset.nix Normal file
View file

@ -0,0 +1,6 @@
# Test 'or' in attrset context
let
attrs = { a = 1; };
in {
test = attrs.a or 999;
}

4
tests/or_simple.nix Normal file
View file

@ -0,0 +1,4 @@
# Simplest 'or' test
let
x = { a = 1; };
in x.a or 2

13
tests/path_concat.nix Normal file
View file

@ -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;
}

Binary file not shown.

View file

@ -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<Node>(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<ConstLookupPathNode>();
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<Node>(ConstPathNode("./test.nix"));
auto import_node = std::make_shared<Node>(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<ImportNode>();
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<ConstPathNode>();
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<Node>(ConstLookupPathNode("nixpkgs"));
auto import_node = std::make_shared<Node>(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<ImportNode>();
TEST_CHECK(loaded_import != nullptr, "Deserialized node is ImportNode");
if (loaded_import && loaded_import->path) {
auto *lookup_node = loaded_import->path->get_if<ConstLookupPathNode>();
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<Node>(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<ConstURINode>();
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<Node>(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<ConstFloatNode>();
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;

View file

@ -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";
}

Binary file not shown.

Binary file not shown.

View file

@ -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";
}

Binary file not shown.

3
tests/uri_test.nix Normal file
View file

@ -0,0 +1,3 @@
https://example.com/path?query=1
#frag