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 "evaluator.h"
|
||||||
#include "nix/expr/eval.hh"
|
#include "nix/expr/eval.hh"
|
||||||
#include "nix/expr/value.hh"
|
#include "nix/expr/value.hh"
|
||||||
#include "nix/util/error.hh"
|
#include "nix/util/url.hh"
|
||||||
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace nix_irc {
|
namespace nix_irc {
|
||||||
|
|
@ -66,11 +64,7 @@ struct Evaluator::Impl {
|
||||||
|
|
||||||
explicit Impl(EvalState& s) : state(s) {}
|
explicit Impl(EvalState& s) : state(s) {}
|
||||||
|
|
||||||
~Impl() {
|
// Destructor not needed - unique_ptr handles cleanup automatically
|
||||||
for (auto& env : environments) {
|
|
||||||
delete env.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IREnvironment* make_env(IREnvironment* parent = nullptr) {
|
IREnvironment* make_env(IREnvironment* parent = nullptr) {
|
||||||
auto env = new IREnvironment(parent);
|
auto env = new IREnvironment(parent);
|
||||||
|
|
@ -108,14 +102,42 @@ struct Evaluator::Impl {
|
||||||
|
|
||||||
if (auto* n = node->get_if<ConstIntNode>()) {
|
if (auto* n = node->get_if<ConstIntNode>()) {
|
||||||
v.mkInt(n->value);
|
v.mkInt(n->value);
|
||||||
|
} else if (auto* n = node->get_if<ConstFloatNode>()) {
|
||||||
|
v.mkFloat(n->value);
|
||||||
} else if (auto* n = node->get_if<ConstStringNode>()) {
|
} else if (auto* n = node->get_if<ConstStringNode>()) {
|
||||||
v.mkString(n->value);
|
v.mkString(n->value);
|
||||||
} else if (auto* n = node->get_if<ConstPathNode>()) {
|
} 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>()) {
|
} else if (auto* n = node->get_if<ConstBoolNode>()) {
|
||||||
v.mkBool(n->value);
|
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();
|
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>()) {
|
} else if (auto* n = node->get_if<VarNode>()) {
|
||||||
Value* bound = env ? env->lookup(n->index) : nullptr;
|
Value* bound = env ? env->lookup(n->index) : nullptr;
|
||||||
if (!bound && env && n->name.has_value()) {
|
if (!bound && env && n->name.has_value()) {
|
||||||
|
|
@ -216,6 +238,22 @@ struct Evaluator::Impl {
|
||||||
v.mkInt((left->integer() + right->integer()).valueWrapping());
|
v.mkInt((left->integer() + right->integer()).valueWrapping());
|
||||||
} else if (left->type() == nString && right->type() == nString) {
|
} else if (left->type() == nString && right->type() == nString) {
|
||||||
v.mkString(std::string(left->c_str()) + std::string(right->c_str()));
|
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 {
|
} else {
|
||||||
state.error<EvalError>("type error in addition").debugThrow();
|
state.error<EvalError>("type error in addition").debugThrow();
|
||||||
}
|
}
|
||||||
|
|
@ -286,10 +324,60 @@ struct Evaluator::Impl {
|
||||||
state.error<EvalError>("type error in comparison").debugThrow();
|
state.error<EvalError>("type error in comparison").debugThrow();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case BinaryOp::CONCAT:
|
case BinaryOp::CONCAT: {
|
||||||
// ++ is list concatenation in Nix; string concat uses ADD (+)
|
// List concatenation: left ++ right
|
||||||
state.error<EvalError>("list concatenation not yet implemented").debugThrow();
|
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;
|
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:
|
default:
|
||||||
state.error<EvalError>("unknown binary operator").debugThrow();
|
state.error<EvalError>("unknown binary operator").debugThrow();
|
||||||
}
|
}
|
||||||
|
|
@ -335,17 +423,16 @@ struct Evaluator::Impl {
|
||||||
} else if (auto* n = node->get_if<LetNode>()) {
|
} else if (auto* n = node->get_if<LetNode>()) {
|
||||||
auto let_env = make_env(env);
|
auto let_env = make_env(env);
|
||||||
for (const auto& [name, expr] : n->bindings) {
|
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);
|
let_env->bind(val);
|
||||||
}
|
}
|
||||||
eval_node(n->body, v, let_env);
|
eval_node(n->body, v, let_env);
|
||||||
} else if (auto* n = node->get_if<LetRecNode>()) {
|
} else if (auto* n = node->get_if<LetRecNode>()) {
|
||||||
auto letrec_env = make_env(env);
|
auto letrec_env = make_env(env);
|
||||||
std::vector<Value*> thunk_vals;
|
|
||||||
|
|
||||||
for (const auto& [name, expr] : n->bindings) {
|
for (const auto& [name, expr] : n->bindings) {
|
||||||
Value* val = make_thunk(expr, letrec_env);
|
Value* val = make_thunk(expr, letrec_env);
|
||||||
thunk_vals.push_back(val);
|
|
||||||
letrec_env->bind(val);
|
letrec_env->bind(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,21 +442,36 @@ struct Evaluator::Impl {
|
||||||
|
|
||||||
IREnvironment* attr_env = env;
|
IREnvironment* attr_env = env;
|
||||||
if (n->recursive) {
|
if (n->recursive) {
|
||||||
|
// For recursive attrsets, create environment where all bindings can
|
||||||
|
// see each other
|
||||||
attr_env = make_env(env);
|
attr_env = make_env(env);
|
||||||
for (const auto& [key, val] : n->attrs) {
|
for (const auto& binding : n->attrs) {
|
||||||
Value* thunk = make_thunk(val, attr_env);
|
if (!binding.is_dynamic()) {
|
||||||
attr_env->bind(thunk);
|
Value* thunk = make_thunk(binding.value, attr_env);
|
||||||
|
attr_env->bind(thunk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [key, val] : n->attrs) {
|
// Attributes should be lazy, so store as thunks and not evaluated values
|
||||||
Value* attr_val = state.allocValue();
|
for (const auto& binding : n->attrs) {
|
||||||
if (n->recursive) {
|
Value* attr_val = make_thunk(binding.value, attr_env);
|
||||||
eval_node(val, *attr_val, 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 {
|
} 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());
|
v.mkAttrs(bindings.finish());
|
||||||
|
|
@ -446,6 +548,21 @@ struct Evaluator::Impl {
|
||||||
}
|
}
|
||||||
|
|
||||||
eval_node(n->body, v, env);
|
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 {
|
} else {
|
||||||
v.mkNull();
|
v.mkNull();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,11 +122,17 @@ struct IRGenerator::Impl {
|
||||||
if (auto* n = node.get_if<AttrsetNode>()) {
|
if (auto* n = node.get_if<AttrsetNode>()) {
|
||||||
AttrsetNode attrs(n->recursive, n->line);
|
AttrsetNode attrs(n->recursive, n->line);
|
||||||
name_resolver.enter_scope();
|
name_resolver.enter_scope();
|
||||||
for (const auto& [key, val] : n->attrs) {
|
for (const auto& binding : n->attrs) {
|
||||||
name_resolver.bind(key);
|
if (!binding.is_dynamic()) {
|
||||||
|
name_resolver.bind(binding.static_name.value());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const auto& [key, val] : n->attrs) {
|
for (const auto& binding : n->attrs) {
|
||||||
attrs.attrs.push_back({key, convert(val)});
|
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();
|
name_resolver.exit_scope();
|
||||||
return std::make_shared<Node>(attrs);
|
return std::make_shared<Node>(attrs);
|
||||||
|
|
@ -162,6 +168,7 @@ struct IRGenerator::Impl {
|
||||||
name_resolver.bind(key);
|
name_resolver.bind(key);
|
||||||
}
|
}
|
||||||
std::vector<std::pair<std::string, std::shared_ptr<Node>>> new_bindings;
|
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) {
|
for (const auto& [key, val] : n->bindings) {
|
||||||
new_bindings.push_back({key, convert(val)});
|
new_bindings.push_back({key, convert(val)});
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +184,7 @@ struct IRGenerator::Impl {
|
||||||
name_resolver.bind(key);
|
name_resolver.bind(key);
|
||||||
}
|
}
|
||||||
std::vector<std::pair<std::string, std::shared_ptr<Node>>> new_bindings;
|
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) {
|
for (const auto& [key, val] : n->bindings) {
|
||||||
new_bindings.push_back({key, convert(val)});
|
new_bindings.push_back({key, convert(val)});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <regex>
|
|
||||||
#include <sstream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|
@ -24,22 +22,27 @@ static std::string read_file(const std::string& path) {
|
||||||
if (!f) {
|
if (!f) {
|
||||||
throw std::runtime_error("Cannot open file: " + path);
|
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);
|
fseek(f, 0, SEEK_END);
|
||||||
long size = ftell(f);
|
long size = ftell(f);
|
||||||
fseek(f, 0, SEEK_SET);
|
fseek(f, 0, SEEK_SET);
|
||||||
std::string content(size, '\0');
|
std::string content(size, '\0');
|
||||||
if (fread(content.data(), 1, size, f) != static_cast<size_t>(size)) {
|
if (fread(content.data(), 1, size, f) != static_cast<size_t>(size)) {
|
||||||
fclose(f);
|
|
||||||
throw std::runtime_error("Failed to read file: " + path);
|
throw std::runtime_error("Failed to read file: " + path);
|
||||||
}
|
}
|
||||||
fclose(f);
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::pair<std::string, std::string> run_command(const std::string& cmd) {
|
static std::pair<std::string, std::string> run_command(const std::string& cmd) {
|
||||||
std::array<char, 256> buffer;
|
std::array<char, 256> buffer;
|
||||||
std::string result;
|
std::string result;
|
||||||
std::string error;
|
|
||||||
|
|
||||||
FILE* pipe = popen(cmd.c_str(), "r");
|
FILE* pipe = popen(cmd.c_str(), "r");
|
||||||
if (!pipe)
|
if (!pipe)
|
||||||
|
|
@ -53,7 +56,7 @@ static std::pair<std::string, std::string> run_command(const std::string& cmd) {
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
throw std::runtime_error("Command failed: " + cmd);
|
throw std::runtime_error("Command failed: " + cmd);
|
||||||
}
|
}
|
||||||
return {result, error};
|
return {result, ""};
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Token {
|
struct Token {
|
||||||
|
|
@ -67,8 +70,13 @@ struct Token {
|
||||||
IDENT,
|
IDENT,
|
||||||
STRING,
|
STRING,
|
||||||
STRING_INTERP,
|
STRING_INTERP,
|
||||||
|
INDENTED_STRING,
|
||||||
|
INDENTED_STRING_INTERP,
|
||||||
PATH,
|
PATH,
|
||||||
|
LOOKUP_PATH,
|
||||||
INT,
|
INT,
|
||||||
|
FLOAT,
|
||||||
|
URI,
|
||||||
BOOL,
|
BOOL,
|
||||||
LET,
|
LET,
|
||||||
IN,
|
IN,
|
||||||
|
|
@ -79,6 +87,7 @@ struct Token {
|
||||||
ASSERT,
|
ASSERT,
|
||||||
WITH,
|
WITH,
|
||||||
INHERIT,
|
INHERIT,
|
||||||
|
IMPORT,
|
||||||
DOT,
|
DOT,
|
||||||
SEMICOLON,
|
SEMICOLON,
|
||||||
COLON,
|
COLON,
|
||||||
|
|
@ -93,6 +102,7 @@ struct Token {
|
||||||
STAR,
|
STAR,
|
||||||
SLASH,
|
SLASH,
|
||||||
CONCAT,
|
CONCAT,
|
||||||
|
MERGE,
|
||||||
EQEQ,
|
EQEQ,
|
||||||
NE,
|
NE,
|
||||||
LT,
|
LT,
|
||||||
|
|
@ -145,6 +155,8 @@ public:
|
||||||
emit(TOKEN(AT));
|
emit(TOKEN(AT));
|
||||||
} else if (c == ',') {
|
} else if (c == ',') {
|
||||||
emit(TOKEN(COMMA));
|
emit(TOKEN(COMMA));
|
||||||
|
} else if (c == '\'' && pos + 1 < input.size() && input[pos + 1] == '\'') {
|
||||||
|
tokenize_indented_string();
|
||||||
} else if (c == '"') {
|
} else if (c == '"') {
|
||||||
tokenize_string();
|
tokenize_string();
|
||||||
}
|
}
|
||||||
|
|
@ -171,6 +183,10 @@ public:
|
||||||
tokens.push_back(TOKEN(CONCAT));
|
tokens.push_back(TOKEN(CONCAT));
|
||||||
pos += 2;
|
pos += 2;
|
||||||
col += 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] == '&') {
|
} else if (c == '&' && pos + 1 < input.size() && input[pos + 1] == '&') {
|
||||||
tokens.push_back(TOKEN(AND));
|
tokens.push_back(TOKEN(AND));
|
||||||
pos += 2;
|
pos += 2;
|
||||||
|
|
@ -197,7 +213,29 @@ public:
|
||||||
emit(TOKEN(SLASH));
|
emit(TOKEN(SLASH));
|
||||||
}
|
}
|
||||||
} else if (c == '<') {
|
} 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 == '>') {
|
} else if (c == '>') {
|
||||||
emit(TOKEN(GT));
|
emit(TOKEN(GT));
|
||||||
} else if (c == '!') {
|
} else if (c == '!') {
|
||||||
|
|
@ -213,17 +251,48 @@ public:
|
||||||
}
|
}
|
||||||
} else if (c == '?') {
|
} else if (c == '?') {
|
||||||
emit(TOKEN(QUESTION));
|
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 == '-') {
|
} else if (c == '-') {
|
||||||
// Check if it's a negative number or minus operator
|
// Check if it's a negative number or minus operator
|
||||||
if (pos + 1 < input.size() && isdigit(input[pos + 1])) {
|
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 {
|
} else {
|
||||||
emit(TOKEN(MINUS));
|
emit(TOKEN(MINUS));
|
||||||
}
|
}
|
||||||
} else if (isdigit(c)) {
|
} else if (isdigit(c)) {
|
||||||
tokenize_int();
|
// Check if it's a float (digit followed by '.')
|
||||||
} else if (isalpha(c) || c == '_') {
|
if (pos + 1 < input.size() && input[pos + 1] == '.') {
|
||||||
tokenize_ident();
|
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 {
|
} else {
|
||||||
pos++;
|
pos++;
|
||||||
col++;
|
col++;
|
||||||
|
|
@ -242,7 +311,7 @@ private:
|
||||||
size_t line;
|
size_t line;
|
||||||
size_t col;
|
size_t col;
|
||||||
|
|
||||||
void emit(Token t) {
|
void emit(const Token& t) {
|
||||||
tokens.push_back(t);
|
tokens.push_back(t);
|
||||||
pos++;
|
pos++;
|
||||||
col++;
|
col++;
|
||||||
|
|
@ -260,8 +329,26 @@ private:
|
||||||
}
|
}
|
||||||
pos++;
|
pos++;
|
||||||
} else if (c == '#') {
|
} else if (c == '#') {
|
||||||
|
// Line comment - skip until newline
|
||||||
while (pos < input.size() && input[pos] != '\n')
|
while (pos < input.size() && input[pos] != '\n')
|
||||||
pos++;
|
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 {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -317,10 +404,175 @@ private:
|
||||||
col += s.size() + 2;
|
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() {
|
void tokenize_path() {
|
||||||
size_t start = pos;
|
size_t start = pos;
|
||||||
while (pos < input.size() && !isspace(input[pos]) && input[pos] != '(' && input[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++;
|
pos++;
|
||||||
}
|
}
|
||||||
std::string path = input.substr(start, pos - start);
|
std::string path = input.substr(start, pos - start);
|
||||||
|
|
@ -328,6 +580,22 @@ private:
|
||||||
col += path.size();
|
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() {
|
void tokenize_int() {
|
||||||
size_t start = pos;
|
size_t start = pos;
|
||||||
if (input[pos] == '-')
|
if (input[pos] == '-')
|
||||||
|
|
@ -339,12 +607,48 @@ private:
|
||||||
col += num.size();
|
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() {
|
void tokenize_ident() {
|
||||||
size_t start = pos;
|
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++;
|
pos++;
|
||||||
std::string ident = input.substr(start, pos - start);
|
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;
|
Token::Type type = Token::IDENT;
|
||||||
if (ident == "let")
|
if (ident == "let")
|
||||||
type = Token::LET;
|
type = Token::LET;
|
||||||
|
|
@ -364,6 +668,8 @@ private:
|
||||||
type = Token::WITH;
|
type = Token::WITH;
|
||||||
else if (ident == "inherit")
|
else if (ident == "inherit")
|
||||||
type = Token::INHERIT;
|
type = Token::INHERIT;
|
||||||
|
else if (ident == "import")
|
||||||
|
type = Token::IMPORT;
|
||||||
else if (ident == "true")
|
else if (ident == "true")
|
||||||
type = Token::BOOL;
|
type = Token::BOOL;
|
||||||
else if (ident == "false")
|
else if (ident == "false")
|
||||||
|
|
@ -410,6 +716,8 @@ public:
|
||||||
// Get operator precedence (higher = tighter binding)
|
// Get operator precedence (higher = tighter binding)
|
||||||
int get_precedence(Token::Type type) {
|
int get_precedence(Token::Type type) {
|
||||||
switch (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:
|
case Token::OR:
|
||||||
return 1;
|
return 1;
|
||||||
case Token::AND:
|
case Token::AND:
|
||||||
|
|
@ -450,6 +758,8 @@ public:
|
||||||
return BinaryOp::DIV;
|
return BinaryOp::DIV;
|
||||||
case Token::CONCAT:
|
case Token::CONCAT:
|
||||||
return BinaryOp::CONCAT;
|
return BinaryOp::CONCAT;
|
||||||
|
case Token::MERGE:
|
||||||
|
return BinaryOp::MERGE;
|
||||||
case Token::EQEQ:
|
case Token::EQEQ:
|
||||||
return BinaryOp::EQ;
|
return BinaryOp::EQ;
|
||||||
case Token::NE:
|
case Token::NE:
|
||||||
|
|
@ -488,6 +798,45 @@ public:
|
||||||
return std::make_shared<Node>(IfNode(cond, then, else_));
|
return std::make_shared<Node>(IfNode(cond, then, else_));
|
||||||
}
|
}
|
||||||
if (consume(Token::LET)) {
|
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);
|
bool is_rec = consume(Token::REC);
|
||||||
std::vector<std::pair<std::string, std::shared_ptr<Node>>> bindings;
|
std::vector<std::pair<std::string, std::shared_ptr<Node>>> bindings;
|
||||||
parse_bindings(bindings);
|
parse_bindings(bindings);
|
||||||
|
|
@ -550,42 +899,30 @@ public:
|
||||||
if (name.type == Token::IDENT) {
|
if (name.type == Token::IDENT) {
|
||||||
advance();
|
advance();
|
||||||
auto attr = std::make_shared<Node>(ConstStringNode(name.value));
|
auto attr = std::make_shared<Node>(ConstStringNode(name.value));
|
||||||
auto result = std::make_shared<Node>(SelectNode(left, attr));
|
left = std::make_shared<Node>(SelectNode(left, attr));
|
||||||
|
// Continue loop to handle multi-dot selections (a.b.c)
|
||||||
if (consume(Token::DOT)) {
|
continue;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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;
|
return left;
|
||||||
}
|
}
|
||||||
|
|
||||||
void parse_expr_attrs(std::shared_ptr<Node>&) {
|
|
||||||
// Extended selection syntax
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<Node> parse_expr2() {
|
std::shared_ptr<Node> parse_expr2() {
|
||||||
std::shared_ptr<Node> left = parse_expr3();
|
std::shared_ptr<Node> left = parse_expr3();
|
||||||
|
|
||||||
|
|
@ -609,6 +946,12 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<Node> parse_expr3() {
|
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
|
// Handle unary operators
|
||||||
if (consume(Token::MINUS)) {
|
if (consume(Token::MINUS)) {
|
||||||
auto operand = parse_expr3();
|
auto operand = parse_expr3();
|
||||||
|
|
@ -646,6 +989,16 @@ public:
|
||||||
return std::make_shared<Node>(ConstIntNode(std::stoll(t.value)));
|
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) {
|
if (t.type == Token::STRING) {
|
||||||
advance();
|
advance();
|
||||||
return std::make_shared<Node>(ConstStringNode(t.value));
|
return std::make_shared<Node>(ConstStringNode(t.value));
|
||||||
|
|
@ -657,11 +1010,27 @@ public:
|
||||||
return parse_string_interp(str_token.value);
|
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) {
|
if (t.type == Token::PATH) {
|
||||||
advance();
|
advance();
|
||||||
return std::make_shared<Node>(ConstPathNode(t.value));
|
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) {
|
if (t.type == Token::BOOL) {
|
||||||
advance();
|
advance();
|
||||||
return std::make_shared<Node>(ConstBoolNode(t.value == "true"));
|
return std::make_shared<Node>(ConstBoolNode(t.value == "true"));
|
||||||
|
|
@ -700,11 +1069,11 @@ public:
|
||||||
// inherit (expr) x → x = expr.x
|
// inherit (expr) x → x = expr.x
|
||||||
auto select = std::make_shared<Node>(
|
auto select = std::make_shared<Node>(
|
||||||
SelectNode(source, std::make_shared<Node>(ConstStringNode(name.value))));
|
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 {
|
} else {
|
||||||
// inherit x → x = x
|
// inherit x → x = x
|
||||||
auto var = std::make_shared<Node>(VarNode(0, name.value));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current().type == Token::IDENT || current().type == Token::STRING) {
|
// Check for dynamic attribute name: ${expr} = value
|
||||||
Token key = current();
|
if (current().type == Token::STRING_INTERP ||
|
||||||
|
current().type == Token::INDENTED_STRING_INTERP) {
|
||||||
|
Token str_token = current();
|
||||||
advance();
|
advance();
|
||||||
std::string key_str = key.value;
|
auto name_expr = parse_string_interp(str_token.value);
|
||||||
|
|
||||||
if (consume(Token::EQUALS)) {
|
if (consume(Token::EQUALS)) {
|
||||||
auto value = parse_expr();
|
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)) {
|
} else if (consume(Token::AT)) {
|
||||||
|
// @ pattern - not affected by nested paths
|
||||||
auto pattern = parse_expr();
|
auto pattern = parse_expr();
|
||||||
auto value = 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> parse_list() {
|
||||||
std::shared_ptr<Node> list = std::make_shared<Node>(ConstNullNode());
|
std::vector<std::shared_ptr<Node>> elements;
|
||||||
|
|
||||||
if (consume(Token::RBRACKET)) {
|
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) {
|
while (current().type != Token::RBRACKET) {
|
||||||
elements.push_back(parse_expr());
|
elements.push_back(parse_expr());
|
||||||
if (!consume(Token::COMMA))
|
if (!consume(Token::RBRACKET)) {
|
||||||
break;
|
// Elements are whitespace-separated in Nix, no comma required
|
||||||
}
|
// But we'll continue parsing until we hit ]
|
||||||
expect(Token::RBRACKET);
|
} else {
|
||||||
|
// Found closing bracket
|
||||||
for (auto it = elements.rbegin(); it != elements.rend(); ++it) {
|
return std::make_shared<Node>(ListNode(elements));
|
||||||
list = std::make_shared<Node>(AppNode(
|
}
|
||||||
std::make_shared<Node>(AppNode(std::make_shared<Node>(VarNode(0, "__list")), *it)),
|
|
||||||
list));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
void parse_bindings(std::vector<std::pair<std::string, std::shared_ptr<Node>>>& bindings) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#include "serializer.h"
|
#include "serializer.h"
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
namespace nix_irc {
|
namespace nix_irc {
|
||||||
|
|
||||||
|
|
@ -31,6 +30,8 @@ struct Serializer::Impl {
|
||||||
NodeType get_node_type(const Node& node) {
|
NodeType get_node_type(const Node& node) {
|
||||||
if (node.holds<ConstIntNode>())
|
if (node.holds<ConstIntNode>())
|
||||||
return NodeType::CONST_INT;
|
return NodeType::CONST_INT;
|
||||||
|
if (node.holds<ConstFloatNode>())
|
||||||
|
return NodeType::CONST_FLOAT;
|
||||||
if (node.holds<ConstStringNode>())
|
if (node.holds<ConstStringNode>())
|
||||||
return NodeType::CONST_STRING;
|
return NodeType::CONST_STRING;
|
||||||
if (node.holds<ConstPathNode>())
|
if (node.holds<ConstPathNode>())
|
||||||
|
|
@ -39,6 +40,10 @@ struct Serializer::Impl {
|
||||||
return NodeType::CONST_BOOL;
|
return NodeType::CONST_BOOL;
|
||||||
if (node.holds<ConstNullNode>())
|
if (node.holds<ConstNullNode>())
|
||||||
return NodeType::CONST_NULL;
|
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>())
|
if (node.holds<VarNode>())
|
||||||
return NodeType::VAR;
|
return NodeType::VAR;
|
||||||
if (node.holds<LambdaNode>())
|
if (node.holds<LambdaNode>())
|
||||||
|
|
@ -49,6 +54,8 @@ struct Serializer::Impl {
|
||||||
return NodeType::BINARY_OP;
|
return NodeType::BINARY_OP;
|
||||||
if (node.holds<UnaryOpNode>())
|
if (node.holds<UnaryOpNode>())
|
||||||
return NodeType::UNARY_OP;
|
return NodeType::UNARY_OP;
|
||||||
|
if (node.holds<ImportNode>())
|
||||||
|
return NodeType::IMPORT;
|
||||||
if (node.holds<AttrsetNode>())
|
if (node.holds<AttrsetNode>())
|
||||||
return NodeType::ATTRSET;
|
return NodeType::ATTRSET;
|
||||||
if (node.holds<SelectNode>())
|
if (node.holds<SelectNode>())
|
||||||
|
|
@ -57,6 +64,8 @@ struct Serializer::Impl {
|
||||||
return NodeType::HAS_ATTR;
|
return NodeType::HAS_ATTR;
|
||||||
if (node.holds<WithNode>())
|
if (node.holds<WithNode>())
|
||||||
return NodeType::WITH;
|
return NodeType::WITH;
|
||||||
|
if (node.holds<ListNode>())
|
||||||
|
return NodeType::LIST;
|
||||||
if (node.holds<IfNode>())
|
if (node.holds<IfNode>())
|
||||||
return NodeType::IF;
|
return NodeType::IF;
|
||||||
if (node.holds<LetNode>())
|
if (node.holds<LetNode>())
|
||||||
|
|
@ -78,6 +87,11 @@ struct Serializer::Impl {
|
||||||
|
|
||||||
if (auto* n = node.get_if<ConstIntNode>()) {
|
if (auto* n = node.get_if<ConstIntNode>()) {
|
||||||
write_u64(static_cast<uint64_t>(n->value));
|
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>()) {
|
} else if (auto* n = node.get_if<ConstStringNode>()) {
|
||||||
write_string(n->value);
|
write_string(n->value);
|
||||||
} else if (auto* n = node.get_if<ConstPathNode>()) {
|
} else if (auto* n = node.get_if<ConstPathNode>()) {
|
||||||
|
|
@ -86,6 +100,10 @@ struct Serializer::Impl {
|
||||||
write_u8(n->value ? 1 : 0);
|
write_u8(n->value ? 1 : 0);
|
||||||
} else if (auto* n = node.get_if<ConstNullNode>()) {
|
} else if (auto* n = node.get_if<ConstNullNode>()) {
|
||||||
// No data for null
|
// 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>()) {
|
} else if (auto* n = node.get_if<VarNode>()) {
|
||||||
write_u32(n->index);
|
write_u32(n->index);
|
||||||
} else if (auto* n = node.get_if<LambdaNode>()) {
|
} else if (auto* n = node.get_if<LambdaNode>()) {
|
||||||
|
|
@ -107,13 +125,22 @@ struct Serializer::Impl {
|
||||||
write_u8(static_cast<uint8_t>(n->op));
|
write_u8(static_cast<uint8_t>(n->op));
|
||||||
if (n->operand)
|
if (n->operand)
|
||||||
write_node(*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>()) {
|
} else if (auto* n = node.get_if<AttrsetNode>()) {
|
||||||
write_u8(n->recursive ? 1 : 0);
|
write_u8(n->recursive ? 1 : 0);
|
||||||
write_u32(n->attrs.size());
|
write_u32(n->attrs.size());
|
||||||
for (const auto& [key, val] : n->attrs) {
|
for (const auto& binding : n->attrs) {
|
||||||
write_string(key);
|
if (binding.is_dynamic()) {
|
||||||
if (val)
|
write_u8(1); // Dynamic flag
|
||||||
write_node(*val);
|
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>()) {
|
} else if (auto* n = node.get_if<SelectNode>()) {
|
||||||
if (n->expr)
|
if (n->expr)
|
||||||
|
|
@ -136,6 +163,12 @@ struct Serializer::Impl {
|
||||||
write_node(*n->attrs);
|
write_node(*n->attrs);
|
||||||
if (n->body)
|
if (n->body)
|
||||||
write_node(*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>()) {
|
} else if (auto* n = node.get_if<IfNode>()) {
|
||||||
if (n->cond)
|
if (n->cond)
|
||||||
write_node(*n->cond);
|
write_node(*n->cond);
|
||||||
|
|
@ -254,6 +287,12 @@ struct Deserializer::Impl {
|
||||||
int64_t val = static_cast<int64_t>(read_u64());
|
int64_t val = static_cast<int64_t>(read_u64());
|
||||||
return std::make_shared<Node>(ConstIntNode(val, line));
|
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: {
|
case NodeType::CONST_STRING: {
|
||||||
std::string val = read_string();
|
std::string val = read_string();
|
||||||
return std::make_shared<Node>(ConstStringNode(val, line));
|
return std::make_shared<Node>(ConstStringNode(val, line));
|
||||||
|
|
@ -268,6 +307,14 @@ struct Deserializer::Impl {
|
||||||
}
|
}
|
||||||
case NodeType::CONST_NULL:
|
case NodeType::CONST_NULL:
|
||||||
return std::make_shared<Node>(ConstNullNode(line));
|
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: {
|
case NodeType::VAR: {
|
||||||
uint32_t index = read_u32();
|
uint32_t index = read_u32();
|
||||||
return std::make_shared<Node>(VarNode(index, "", line));
|
return std::make_shared<Node>(VarNode(index, "", line));
|
||||||
|
|
@ -293,14 +340,25 @@ struct Deserializer::Impl {
|
||||||
auto operand = read_node();
|
auto operand = read_node();
|
||||||
return std::make_shared<Node>(UnaryOpNode(op, operand, line));
|
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: {
|
case NodeType::ATTRSET: {
|
||||||
bool recursive = read_u8() != 0;
|
bool recursive = read_u8() != 0;
|
||||||
uint32_t num_attrs = read_u32();
|
uint32_t num_attrs = read_u32();
|
||||||
AttrsetNode attrs(recursive, line);
|
AttrsetNode attrs(recursive, line);
|
||||||
for (uint32_t i = 0; i < num_attrs; i++) {
|
for (uint32_t i = 0; i < num_attrs; i++) {
|
||||||
std::string key = read_string();
|
uint8_t is_dynamic = read_u8();
|
||||||
auto val = read_node();
|
if (is_dynamic) {
|
||||||
attrs.attrs.push_back({key, val});
|
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));
|
return std::make_shared<Node>(std::move(attrs));
|
||||||
}
|
}
|
||||||
|
|
@ -326,6 +384,15 @@ struct Deserializer::Impl {
|
||||||
auto body = read_node();
|
auto body = read_node();
|
||||||
return std::make_shared<Node>(WithNode(attrs, body, line));
|
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: {
|
case NodeType::IF: {
|
||||||
auto cond = read_node();
|
auto cond = read_node();
|
||||||
auto then_branch = read_node();
|
auto then_branch = read_node();
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@
|
||||||
#define NIX_IRC_TYPES_H
|
#define NIX_IRC_TYPES_H
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <fstream>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -19,19 +17,24 @@ constexpr uint32_t IR_VERSION = 2;
|
||||||
|
|
||||||
enum class NodeType : uint8_t {
|
enum class NodeType : uint8_t {
|
||||||
CONST_INT = 0x01,
|
CONST_INT = 0x01,
|
||||||
|
CONST_FLOAT = 0x06,
|
||||||
CONST_STRING = 0x02,
|
CONST_STRING = 0x02,
|
||||||
CONST_PATH = 0x03,
|
CONST_PATH = 0x03,
|
||||||
CONST_BOOL = 0x04,
|
CONST_BOOL = 0x04,
|
||||||
CONST_NULL = 0x05,
|
CONST_NULL = 0x05,
|
||||||
|
CONST_URI = 0x07,
|
||||||
|
CONST_LOOKUP_PATH = 0x08,
|
||||||
VAR = 0x10,
|
VAR = 0x10,
|
||||||
LAMBDA = 0x20,
|
LAMBDA = 0x20,
|
||||||
APP = 0x21,
|
APP = 0x21,
|
||||||
BINARY_OP = 0x22,
|
BINARY_OP = 0x22,
|
||||||
UNARY_OP = 0x23,
|
UNARY_OP = 0x23,
|
||||||
|
IMPORT = 0x24,
|
||||||
ATTRSET = 0x30,
|
ATTRSET = 0x30,
|
||||||
SELECT = 0x31,
|
SELECT = 0x31,
|
||||||
HAS_ATTR = 0x34,
|
HAS_ATTR = 0x34,
|
||||||
WITH = 0x32,
|
WITH = 0x32,
|
||||||
|
LIST = 0x33,
|
||||||
IF = 0x40,
|
IF = 0x40,
|
||||||
LET = 0x50,
|
LET = 0x50,
|
||||||
LETREC = 0x51,
|
LETREC = 0x51,
|
||||||
|
|
@ -41,7 +44,23 @@ enum class NodeType : uint8_t {
|
||||||
ERROR = 0xFF
|
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 };
|
enum class UnaryOp : uint8_t { NEG, NOT };
|
||||||
|
|
||||||
|
|
@ -77,6 +96,24 @@ struct ConstNullNode {
|
||||||
ConstNullNode(uint32_t l = 0) : line(l) {}
|
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 {
|
struct VarNode {
|
||||||
uint32_t index = 0;
|
uint32_t index = 0;
|
||||||
std::optional<std::string> name;
|
std::optional<std::string> name;
|
||||||
|
|
@ -116,8 +153,24 @@ struct UnaryOpNode {
|
||||||
UnaryOpNode(UnaryOp o, std::shared_ptr<Node> operand, uint32_t l = 0);
|
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 {
|
struct AttrsetNode {
|
||||||
std::vector<std::pair<std::string, std::shared_ptr<Node>>> attrs;
|
std::vector<AttrBinding> attrs;
|
||||||
bool recursive = false;
|
bool recursive = false;
|
||||||
uint32_t line = 0;
|
uint32_t line = 0;
|
||||||
AttrsetNode(bool rec = false, uint32_t l = 0) : recursive(rec), line(l) {}
|
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);
|
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 {
|
struct ThunkNode {
|
||||||
std::shared_ptr<Node> expr;
|
std::shared_ptr<Node> expr;
|
||||||
uint32_t line = 0;
|
uint32_t line = 0;
|
||||||
|
|
@ -186,13 +245,21 @@ struct ForceNode {
|
||||||
ForceNode(std::shared_ptr<Node> e, uint32_t l = 0);
|
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
|
// Node wraps a variant for type-safe AST
|
||||||
class Node {
|
class Node {
|
||||||
public:
|
public:
|
||||||
using Variant = std::variant<ConstIntNode, ConstStringNode, ConstPathNode, ConstBoolNode,
|
using Variant = std::variant<ConstIntNode, ConstFloatNode, ConstStringNode, ConstPathNode,
|
||||||
ConstNullNode, VarNode, LambdaNode, AppNode, BinaryOpNode,
|
ConstBoolNode, ConstNullNode, ConstURINode, ConstLookupPathNode,
|
||||||
UnaryOpNode, AttrsetNode, SelectNode, HasAttrNode, WithNode, IfNode,
|
VarNode, LambdaNode, AppNode, BinaryOpNode, UnaryOpNode, ImportNode,
|
||||||
LetNode, LetRecNode, AssertNode, ThunkNode, ForceNode>;
|
AttrsetNode, SelectNode, HasAttrNode, WithNode, IfNode, LetNode,
|
||||||
|
LetRecNode, AssertNode, ThunkNode, ForceNode, ListNode>;
|
||||||
|
|
||||||
Variant data;
|
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)
|
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) {}
|
: 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 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) {}
|
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;
|
<< 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() {
|
int main() {
|
||||||
std::cout << "=== Regression Tests for Nixir ===" << std::endl << std::endl;
|
std::cout << "=== Regression Tests for Nixir ===" << std::endl << std::endl;
|
||||||
|
|
||||||
|
|
@ -178,6 +288,21 @@ int main() {
|
||||||
test_parser_expect_in_speculative_parsing();
|
test_parser_expect_in_speculative_parsing();
|
||||||
std::cout << std::endl;
|
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 << "=== Tests Complete ===" << std::endl;
|
||||||
std::cout << "Failures: " << failures << std::endl;
|
std::cout << "Failures: " << failures << std::endl;
|
||||||
return failures > 0 ? 1 : 0;
|
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
|
# Multiple interpolations
|
||||||
multi = "x is ${x} and name is ${name}";
|
multi = "x is ${x} and name is ${name}";
|
||||||
|
|
||||||
# Nested expression
|
# Expression evaluation in interpolation
|
||||||
nested = "Result: ${
|
computed = "x + 10 = ${x + 10}";
|
||||||
if bool_val
|
|
||||||
then "yes"
|
|
||||||
else "no"
|
|
||||||
}";
|
|
||||||
|
|
||||||
# Just a string (no interpolation)
|
bool_check = "${bool_val} is true!";
|
||||||
|
|
||||||
|
# Just a string, no interpolation
|
||||||
plain = "plain text";
|
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