Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ jobs:
- name: Set up the llvm repository
run: |
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo apt-add-repository "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main"
- run: sudo apt-get update && sudo apt-get install --no-install-recommends clang-format-19
- run: find . -name "*.h" -o -name "*.cc" | xargs clang-format-19 -style=file -i
sudo apt-add-repository "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"
- run: sudo apt-get update && sudo apt-get install --no-install-recommends clang-format-21
- run: find . -name "*.h" -o -name "*.cc" | xargs clang-format-21 -style=file -i
- run: git diff --exit-code

buildifier:
Expand Down
2 changes: 1 addition & 1 deletion starlark/ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ constexpr bool ListComp::operator==(ListComp const &o) const {
}

struct AssignStmt {
Expression target;
Identifier target;
Expression value;
constexpr bool operator==(AssignStmt const &) const = default;
};
Expand Down
92 changes: 92 additions & 0 deletions starlark/interpreter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2026 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause

#ifndef STARLARK_INTERPRETER_H_
#define STARLARK_INTERPRETER_H_

#include "starlark/ast.h"

#include <map>
#include <optional>
#include <string>
#include <variant>
#include <vector>

namespace starlark {

struct Value {
std::variant<std::string, std::vector<Value>> v;
constexpr bool operator==(Value const &) const = default;
};

// TODO(robinlinden): Error-handling.
class Interpreter {
public:
std::map<std::string, Value> variables;

std::optional<Value> run(Program const &program) {
std::optional<Value> result;

for (auto const &stmt : program.statements) {
result = std::visit([this](auto const &s) { return run(s); }, stmt);
}

return result;
}

std::optional<Value> run(auto const &) {
// TODO(robinlinden): Delete this.
return std::nullopt;
}

std::optional<Value> run(Statement const &stmt) {
return std::visit([this](auto const &s) { return run(s); }, stmt);
}

std::optional<Value> run(AssignStmt const &stmt) {
auto value = run(stmt.value);
if (!value) {
return std::nullopt;
}

return variables[stmt.target.name] = *value;
}

std::optional<Value> run(ExpressionStmt const &stmt) { return run(stmt.expr); }

std::optional<Value> run(Expression const &expr) {
return std::visit([this](auto const &e) { return run(e); }, expr);
}

std::optional<Value> run(Identifier const &ident) {
auto it = variables.find(ident.name);
if (it == variables.end()) {
return std::nullopt;
}

return it->second;
}

std::optional<Value> run(StringLiteral const &str) { return Value{str.value}; }

std::optional<Value> run(ListExpr const &list) {
std::vector<Value> elements;
elements.reserve(list.elements.size());

for (auto const &elem : list.elements) {
auto value = run(elem);
if (!value) {
return std::nullopt;
}

elements.push_back(std::move(*value));
}

return Value{.v = std::move(elements)};
}
};

} // namespace starlark

#endif
98 changes: 98 additions & 0 deletions starlark/interpreter_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2026 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause

#include "starlark/interpreter.h"

#include "starlark/ast.h"

#include "etest/etest2.h"

auto run(auto starlark) { return starlark::Interpreter{}.run(starlark); }

int main() {
using namespace starlark;

etest::Suite s{};

s.add_test("Program", [](etest::IActions &a) {
Program program{
.statements{
AssignStmt{
.target = Identifier{"B"},
.value = StringLiteral{"hello"},
},
AssignStmt{
.target = Identifier{"A"},
.value = Identifier{"B"},
},
ExpressionStmt{
.expr = Identifier{"A"},
},
},
};

a.expect_eq(run(program), Value{std::string{"hello"}});

// Empty programs are fine too.
a.expect_eq(run(Program{.statements{}}), std::nullopt);
});

s.add_test("AssignStmt", [](etest::IActions &a) {
auto stmt = AssignStmt{
.target = Identifier{"foo"},
.value = StringLiteral{"bar"},
};

a.expect_eq(run(stmt), Value{std::string{"bar"}});
});

s.add_test("ExpressionStmt", [](etest::IActions &a) {
auto stmt = ExpressionStmt{
.expr = StringLiteral{"hello world"},
};

a.expect_eq(run(stmt), Value{std::string{"hello world"}});
});

s.add_test("Expression", [](etest::IActions &a) {
auto expr = Expression{StringLiteral{"hello world"}};
a.expect_eq(run(expr), Value{std::string{"hello world"}});
});

s.add_test("Identifier", [](etest::IActions &a) {
Interpreter interpreter{};
interpreter.variables["greeting"] = Value{std::string{"hello"}};

auto ident = Identifier{"greeting"};
a.expect_eq(interpreter.run(ident), Value{std::string{"hello"}});

auto missing_ident = Identifier{"missing"};
a.expect_eq(interpreter.run(missing_ident).has_value(), false);
});

s.add_test("StringLiteral", [](etest::IActions &a) {
auto str_lit = StringLiteral{"hello world"};
a.expect_eq(run(str_lit), Value{std::string{"hello world"}});
});

s.add_test("ListExpr", [](etest::IActions &a) {
auto list_expr = ListExpr{
.elements{
Expression{StringLiteral{"foo"}},
Expression{StringLiteral{"bar"}},
Expression{StringLiteral{"baz"}},
},
};

a.expect_eq(
run(list_expr),
Value{std::vector<Value>{
Value{std::string{"foo"}},
Value{std::string{"bar"}},
Value{std::string{"baz"}},
}});
});

return s.run();
}
10 changes: 9 additions & 1 deletion starlark/parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ class Parser {
if (auto expr = parse_expression(token); expr.has_value()) {
auto next = next_token();
if (next.has_value() && std::holds_alternative<token::Equals>(*next)) {
if (!std::holds_alternative<Identifier>(*expr)) {
std::cerr << "Left-hand side of assignment must be an identifier.\n";
return std::nullopt;
}

auto rhs_token = next_token();
if (!rhs_token) {
std::cerr << "Unexpected end of input in assignment.\n";
Expand All @@ -60,7 +65,10 @@ class Parser {
}

program.statements.push_back(
AssignStmt{.target = std::move(*expr), .value = std::move(*rhs)});
AssignStmt{
.target = std::move(std::get<Identifier>(*expr)),
.value = std::move(*rhs),
});
continue;
} else if (next.has_value()) {
reconsume(std::move(*next));
Expand Down
4 changes: 3 additions & 1 deletion starlark/parser_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,12 @@ int main() {
"[e for y in '",

// AssignStmt
// Tokenization error in target.
// Tokenization error in value.
"A = \"",
// Parse error in value.
"A = foo(",
// Non-ident target.
"\"A\" = B",
});

etest::Suite s{};
Expand Down