Compare commits
8 commits
b49044c9a5
...
121803b13c
| Author | SHA1 | Date | |
|---|---|---|---|
|
121803b13c |
|||
|
00a3d2e585 |
|||
|
ed8f637c99 |
|||
|
77aa67c7e0 |
|||
|
a6aade6c11 |
|||
|
3c1ce0fd31 |
|||
|
59fcd3ef92 |
|||
|
38c13de01d |
35 changed files with 1083 additions and 116 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
8
tests/ancient_let.nix
Normal 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
12
tests/block_comments.nix
Normal 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
15
tests/dynamic_attrs.nix
Normal 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
1
tests/float_test.nix
Normal file
|
|
@ -0,0 +1 @@
|
|||
1.5
|
||||
11
tests/home_path.nix
Normal file
11
tests/home_path.nix
Normal 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;
|
||||
}
|
||||
BIN
tests/if.nixir
BIN
tests/if.nixir
Binary file not shown.
3
tests/import_lookup.nix
Normal file
3
tests/import_lookup.nix
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Test import with lookup path
|
||||
# Common pattern: import <nixpkgs> { }
|
||||
import <nixpkgs>
|
||||
11
tests/import_simple.nix
Normal file
11
tests/import_simple.nix
Normal 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
31
tests/indented_string.nix
Normal 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;
|
||||
}
|
||||
BIN
tests/let.nixir
BIN
tests/let.nixir
Binary file not shown.
15
tests/list_concat.nix
Normal file
15
tests/list_concat.nix
Normal 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
9
tests/lookup_path.nix
Normal 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>
|
||||
3
tests/lookup_path_nested.nix
Normal file
3
tests/lookup_path_nested.nix
Normal 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
2
tests/merge.nix
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Test attrset merge operator (//)
|
||||
{a = {x = 1;} // {y = 2;};}
|
||||
13
tests/nested_attrs.nix
Normal file
13
tests/nested_attrs.nix
Normal 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
6
tests/or_in_attrset.nix
Normal 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
4
tests/or_simple.nix
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Simplest 'or' test
|
||||
let
|
||||
x = { a = 1; };
|
||||
in x.a or 2
|
||||
13
tests/path_concat.nix
Normal file
13
tests/path_concat.nix
Normal 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.
|
|
@ -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;
|
||||
|
|
|
|||
16
tests/select_or_default.nix
Normal file
16
tests/select_or_default.nix
Normal 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.
|
|
@ -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
3
tests/uri_test.nix
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
https://example.com/path?query=1
|
||||
#frag
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue