diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1c54835..46f67df 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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: diff --git a/starlark/ast.h b/starlark/ast.h index eeab81f..22d17a3 100644 --- a/starlark/ast.h +++ b/starlark/ast.h @@ -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; }; diff --git a/starlark/interpreter.h b/starlark/interpreter.h new file mode 100644 index 0000000..2c7fb9e --- /dev/null +++ b/starlark/interpreter.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2026 Robin Lindén +// +// SPDX-License-Identifier: BSD-2-Clause + +#ifndef STARLARK_INTERPRETER_H_ +#define STARLARK_INTERPRETER_H_ + +#include "starlark/ast.h" + +#include +#include +#include +#include +#include + +namespace starlark { + +struct Value { + std::variant> v; + constexpr bool operator==(Value const &) const = default; +}; + +// TODO(robinlinden): Error-handling. +class Interpreter { + public: + std::map variables; + + std::optional run(Program const &program) { + std::optional result; + + for (auto const &stmt : program.statements) { + result = std::visit([this](auto const &s) { return run(s); }, stmt); + } + + return result; + } + + std::optional run(auto const &) { + // TODO(robinlinden): Delete this. + return std::nullopt; + } + + std::optional run(Statement const &stmt) { + return std::visit([this](auto const &s) { return run(s); }, stmt); + } + + std::optional run(AssignStmt const &stmt) { + auto value = run(stmt.value); + if (!value) { + return std::nullopt; + } + + return variables[stmt.target.name] = *value; + } + + std::optional run(ExpressionStmt const &stmt) { return run(stmt.expr); } + + std::optional run(Expression const &expr) { + return std::visit([this](auto const &e) { return run(e); }, expr); + } + + std::optional run(Identifier const &ident) { + auto it = variables.find(ident.name); + if (it == variables.end()) { + return std::nullopt; + } + + return it->second; + } + + std::optional run(StringLiteral const &str) { return Value{str.value}; } + + std::optional run(ListExpr const &list) { + std::vector 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 diff --git a/starlark/interpreter_test.cc b/starlark/interpreter_test.cc new file mode 100644 index 0000000..1572756 --- /dev/null +++ b/starlark/interpreter_test.cc @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2026 Robin Lindén +// +// 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{std::string{"foo"}}, + Value{std::string{"bar"}}, + Value{std::string{"baz"}}, + }}); + }); + + return s.run(); +} diff --git a/starlark/parser.h b/starlark/parser.h index d4142d9..6c656f8 100644 --- a/starlark/parser.h +++ b/starlark/parser.h @@ -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(*next)) { + if (!std::holds_alternative(*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"; @@ -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(*expr)), + .value = std::move(*rhs), + }); continue; } else if (next.has_value()) { reconsume(std::move(*next)); diff --git a/starlark/parser_test.cc b/starlark/parser_test.cc index 84ec8b3..e4fa1ee 100644 --- a/starlark/parser_test.cc +++ b/starlark/parser_test.cc @@ -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{};