irc: improve multi-line strings; complete list concat and dynamic attrs
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I64e53c68d90b62f3ca306865ceda32af6a6a6964
This commit is contained in:
parent
00a3d2e585
commit
121803b13c
5 changed files with 204 additions and 61 deletions
|
|
@ -130,6 +130,14 @@ struct Evaluator::Impl {
|
|||
// We can use EvalState's searchPath to resolve
|
||||
auto path = state.findFile(n->value);
|
||||
v.mkPath(path);
|
||||
} else if (auto* n = node->get_if<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()) {
|
||||
|
|
@ -316,15 +324,34 @@ struct Evaluator::Impl {
|
|||
state.error<EvalError>("type error in comparison").debugThrow();
|
||||
}
|
||||
break;
|
||||
case BinaryOp::CONCAT:
|
||||
// TODO: ++ list concatenation requires accessing private Nix Value payload
|
||||
// For now, delegate to Nix's concatLists or implement via builtins
|
||||
// Parser recognizes ++ but evaluator not yet fully implemented
|
||||
state
|
||||
.error<EvalError>(
|
||||
"list concatenation (++) not yet fully implemented - use builtins.concatLists")
|
||||
.debugThrow();
|
||||
case BinaryOp::CONCAT: {
|
||||
// List concatenation: left ++ right
|
||||
if (left->type() != nList || right->type() != nList) {
|
||||
state.error<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) {
|
||||
|
|
@ -418,16 +445,33 @@ struct Evaluator::Impl {
|
|||
// For recursive attrsets, create environment where all bindings can
|
||||
// see each other
|
||||
attr_env = make_env(env);
|
||||
for (const auto& [key, val] : n->attrs) {
|
||||
Value* thunk = make_thunk(val, attr_env);
|
||||
attr_env->bind(thunk);
|
||||
for (const auto& binding : n->attrs) {
|
||||
if (!binding.is_dynamic()) {
|
||||
Value* thunk = make_thunk(binding.value, attr_env);
|
||||
attr_env->bind(thunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attributes should be lazy, so store as thunks and not evaluated values
|
||||
for (const auto& [key, val] : n->attrs) {
|
||||
Value* attr_val = make_thunk(val, attr_env);
|
||||
bindings.insert(state.symbols.create(key), attr_val);
|
||||
for (const auto& binding : n->attrs) {
|
||||
Value* attr_val = make_thunk(binding.value, attr_env);
|
||||
|
||||
if (binding.is_dynamic()) {
|
||||
// Evaluate key expression to get attribute name
|
||||
Value* key_val = state.allocValue();
|
||||
eval_node(binding.dynamic_name, *key_val, attr_env);
|
||||
force(key_val);
|
||||
|
||||
if (key_val->type() != nString) {
|
||||
state.error<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 {
|
||||
bindings.insert(state.symbols.create(binding.static_name.value()), attr_val);
|
||||
}
|
||||
}
|
||||
|
||||
v.mkAttrs(bindings.finish());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -426,7 +426,30 @@ private:
|
|||
pos += 3;
|
||||
continue;
|
||||
} else if (pos + 2 < input.size() && input[pos + 2] == '\\') {
|
||||
// ''\ -> escape for backslash
|
||||
// ''\ -> check what follows
|
||||
if (pos + 3 < input.size()) {
|
||||
char next = input[pos + 3];
|
||||
if (next == 'n') {
|
||||
raw_content += '\n';
|
||||
pos += 4;
|
||||
continue;
|
||||
} else if (next == 'r') {
|
||||
raw_content += '\r';
|
||||
pos += 4;
|
||||
continue;
|
||||
} else if (next == 't') {
|
||||
raw_content += '\t';
|
||||
pos += 4;
|
||||
continue;
|
||||
} else if (next == ' ' || next == '\t') {
|
||||
// ''\ before whitespace - preserve the whitespace (mark it specially)
|
||||
raw_content += "\x01"; // Use control char as marker for preserved whitespace
|
||||
raw_content += next;
|
||||
pos += 4;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Default: literal backslash
|
||||
raw_content += '\\';
|
||||
pos += 3;
|
||||
continue;
|
||||
|
|
@ -486,12 +509,17 @@ private:
|
|||
}
|
||||
|
||||
// Find minimum indentation (spaces/tabs at start of non-empty lines)
|
||||
// \x01 marker indicates preserved whitespace (from ''\ escape)
|
||||
size_t min_indent = std::string::npos;
|
||||
for (const auto& line : lines) {
|
||||
if (line.empty())
|
||||
continue; // Skip empty lines when calculating indentation
|
||||
size_t indent = 0;
|
||||
for (char c : line) {
|
||||
for (size_t i = 0; i < line.size(); i++) {
|
||||
char c = line[i];
|
||||
// If we hit the preserved whitespace marker, stop counting indentation
|
||||
if (c == '\x01')
|
||||
break;
|
||||
if (c == ' ' || c == '\t')
|
||||
indent++;
|
||||
else
|
||||
|
|
@ -504,7 +532,7 @@ private:
|
|||
if (min_indent == std::string::npos)
|
||||
min_indent = 0;
|
||||
|
||||
// Strip min_indent from all lines
|
||||
// Strip min_indent from all lines and remove \x01 markers
|
||||
std::string result;
|
||||
for (size_t i = 0; i < lines.size(); i++) {
|
||||
const auto& line = lines[i];
|
||||
|
|
@ -513,9 +541,25 @@ private:
|
|||
if (i + 1 < lines.size())
|
||||
result += '\n';
|
||||
} else {
|
||||
// Strip indentation
|
||||
size_t skip = std::min(min_indent, line.size());
|
||||
result += line.substr(skip);
|
||||
// Strip indentation, being careful about \x01 markers
|
||||
size_t skip = 0;
|
||||
size_t pos = 0;
|
||||
while (skip < min_indent && pos < line.size()) {
|
||||
if (line[pos] == '\x01') {
|
||||
// Hit preserved whitespace marker - don't strip any more
|
||||
break;
|
||||
}
|
||||
skip++;
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Add the rest of the line, removing \x01 markers
|
||||
for (size_t j = pos; j < line.size(); j++) {
|
||||
if (line[j] != '\x01') {
|
||||
result += line[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (i + 1 < lines.size())
|
||||
result += '\n';
|
||||
}
|
||||
|
|
@ -1025,11 +1069,11 @@ public:
|
|||
// inherit (expr) x → x = expr.x
|
||||
auto select = std::make_shared<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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1046,11 +1090,8 @@ public:
|
|||
|
||||
if (consume(Token::EQUALS)) {
|
||||
auto value = parse_expr();
|
||||
// For dynamic attrs, we use special marker in key and store expr as value
|
||||
// This will need runtime evaluation - store as special node
|
||||
// For now, convert to string at parse time if possible
|
||||
// TODO: Full dynamic attr support needs IR node for dynamic keys
|
||||
attrs.attrs.push_back({"__dynamic__", value});
|
||||
// Dynamic attribute - name is evaluated at runtime
|
||||
attrs.attrs.push_back(AttrBinding(name_expr, value));
|
||||
}
|
||||
} else if (current().type == Token::IDENT || current().type == Token::STRING ||
|
||||
current().type == Token::INDENTED_STRING) {
|
||||
|
|
@ -1076,22 +1117,22 @@ public:
|
|||
// Desugar nested paths: a.b.c = v becomes a = { b = { c = v; }; }
|
||||
if (path.size() == 1) {
|
||||
// Simple case: just one key
|
||||
attrs.attrs.push_back({path[0], value});
|
||||
attrs.attrs.push_back(AttrBinding(path[0], value));
|
||||
} else {
|
||||
// Nested case: build nested attrsets from right to left
|
||||
auto nested = value;
|
||||
for (int i = path.size() - 1; i > 0; i--) {
|
||||
auto inner_attrs = AttrsetNode(false);
|
||||
inner_attrs.attrs.push_back({path[i], nested});
|
||||
inner_attrs.attrs.push_back(AttrBinding(path[i], nested));
|
||||
nested = std::make_shared<Node>(std::move(inner_attrs));
|
||||
}
|
||||
attrs.attrs.push_back({path[0], nested});
|
||||
attrs.attrs.push_back(AttrBinding(path[0], nested));
|
||||
}
|
||||
} else if (consume(Token::AT)) {
|
||||
// @ pattern - not affected by nested paths
|
||||
auto pattern = parse_expr();
|
||||
auto value = parse_expr();
|
||||
attrs.attrs.push_back({path[0], value});
|
||||
attrs.attrs.push_back(AttrBinding(path[0], value));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1111,27 +1152,25 @@ public:
|
|||
}
|
||||
|
||||
std::shared_ptr<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) {
|
||||
|
|
|
|||
|
|
@ -64,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>())
|
||||
|
|
@ -129,10 +131,16 @@ struct Serializer::Impl {
|
|||
} 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)
|
||||
|
|
@ -155,6 +163,12 @@ struct Serializer::Impl {
|
|||
write_node(*n->attrs);
|
||||
if (n->body)
|
||||
write_node(*n->body);
|
||||
} else if (auto* n = node.get_if<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);
|
||||
|
|
@ -335,9 +349,16 @@ struct Deserializer::Impl {
|
|||
uint32_t num_attrs = read_u32();
|
||||
AttrsetNode attrs(recursive, line);
|
||||
for (uint32_t i = 0; i < num_attrs; i++) {
|
||||
std::string key = read_string();
|
||||
auto val = read_node();
|
||||
attrs.attrs.push_back({key, val});
|
||||
uint8_t is_dynamic = read_u8();
|
||||
if (is_dynamic) {
|
||||
auto key_expr = read_node();
|
||||
auto val = read_node();
|
||||
attrs.attrs.push_back(AttrBinding(key_expr, val));
|
||||
} else {
|
||||
std::string key = read_string();
|
||||
auto val = read_node();
|
||||
attrs.attrs.push_back(AttrBinding(key, val));
|
||||
}
|
||||
}
|
||||
return std::make_shared<Node>(std::move(attrs));
|
||||
}
|
||||
|
|
@ -363,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();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ enum class NodeType : uint8_t {
|
|||
SELECT = 0x31,
|
||||
HAS_ATTR = 0x34,
|
||||
WITH = 0x32,
|
||||
LIST = 0x33,
|
||||
IF = 0x40,
|
||||
LET = 0x50,
|
||||
LETREC = 0x51,
|
||||
|
|
@ -152,8 +153,24 @@ struct UnaryOpNode {
|
|||
UnaryOpNode(UnaryOp o, std::shared_ptr<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) {}
|
||||
|
|
@ -228,14 +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, ConstFloatNode, ConstStringNode, ConstPathNode, ConstBoolNode,
|
||||
ConstNullNode, ConstURINode, ConstLookupPathNode, VarNode, LambdaNode, AppNode,
|
||||
BinaryOpNode, UnaryOpNode, ImportNode, 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;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue