Compare commits

...

6 Commits

Author SHA1 Message Date
9c5a3397d9 Add break continue 2026-01-16 00:01:23 +08:00
df84159ed8 Fix for 2026-01-15 23:51:19 +08:00
896459bdfa Add error info 2026-01-15 23:44:48 +08:00
68ba6f9293 Fix comments lexer 2026-01-15 23:44:34 +08:00
06612e2824 Fix stmt not consume semicolon 2026-01-15 23:44:13 +08:00
0248f47218 Add cli 2026-01-15 23:25:24 +08:00
19 changed files with 226 additions and 55 deletions

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@ Makefile
# Generated files # Generated files
*.log *.log
*.out *.out
.cache/
# macOS # macOS
.DS_Store .DS_Store

View File

@@ -1,37 +1,58 @@
cmake_minimum_required(VERSION 3.30) cmake_minimum_required(VERSION 3.30)
project(camellya) project(camellya)
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
option(CAMELLYA_BUILD_TESTS "Build tests" ON)
option(CAMELLYA_BUILD_STATIC "Build static library" ON)
option(CAMELLYA_BUILD_CLI "Build command line interface" ON)
# Library sources # Library sources
set(LIB_SOURCES set(LIB_SOURCES
library.cpp src/lexer.cpp
lexer.cpp src/parser.cpp
parser.cpp src/value.cpp
value.cpp src/interpreter.cpp
interpreter.cpp src/state.cpp
state.cpp
) )
# Library headers
set(LIB_HEADERS set(LIB_HEADERS
library.h src/camellya.h
lexer.h src/lexer.h
parser.h src/parser.h
ast.h src/ast.h
value.h src/value.h
interpreter.h src/interpreter.h
state.h src/state.h
) )
# Build static library if(CAMELLYA_BUILD_STATIC)
add_library(camellya STATIC ${LIB_SOURCES} ${LIB_HEADERS}) add_library(libcamellya STATIC ${LIB_SOURCES} ${LIB_HEADERS})
elseif()
add_library(libcamellya SHARED ${LIB_SOURCES} ${LIB_HEADERS})
endif()
target_include_directories(libcamellya PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
if(CAMELLYA_BUILD_CLI)
add_executable(camellya
cli/main.cpp
)
target_link_libraries(camellya
PRIVATE
libcamellya
)
endif()
if(CAMELLYA_BUILD_TESTS)
include(FetchContent) include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
Catch2 Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.6.0 GIT_TAG v3.12.0
) )
FetchContent_MakeAvailable(Catch2) FetchContent_MakeAvailable(Catch2)
@@ -49,8 +70,9 @@ target_include_directories(camellya_tests
target_link_libraries(camellya_tests target_link_libraries(camellya_tests
PRIVATE PRIVATE
camellya libcamellya
Catch2::Catch2WithMain Catch2::Catch2WithMain
) )
add_test(NAME camellya_tests COMMAND camellya_tests) add_test(NAME camellya_tests COMMAND camellya_tests)
endif()

22
cli/main.cpp Normal file
View File

@@ -0,0 +1,22 @@
#include "camellya.h"
#include <iostream>
#include <format>
#include <chrono>
int main(int argc, char** argv) {
if(argc < 2){
std::cout << std::format("Usage: camellya <script> \n") << std::endl;
return 0;
}
std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
camellya::State state;
bool success = state.do_file(argv[1]);
if (!success) {
std::cerr << "Error: " << state.get_error() << std::endl;
return 1;
}
std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << std::format("Execution completed in {} seconds. \n", duration.count()) << std::endl;
return 0;
}

View File

@@ -34,6 +34,14 @@ print("List:", numbers);
print("First element (index 0):", numbers[0]); print("First element (index 0):", numbers[0]);
print("Third element (index 2):", numbers[2]); print("Third element (index 2):", numbers[2]);
for(number i = 0; i < len(numbers); i = i + 1) {
print("List element", numbers[i]);
}
while(true) {
print("test break");
break;
}
// Test maps // Test maps
print("\n=== Map Demo ==="); print("\n=== Map Demo ===");
map config = {"host": "localhost", "port": "8080"}; map config = {"host": "localhost", "port": "8080"};

View File

@@ -1,4 +0,0 @@
#include "library.h"
// Implementation file for Camellya scripting language
// All implementation is in separate source files

View File

@@ -183,6 +183,14 @@ struct ReturnStmt : public Stmt {
explicit ReturnStmt(ExprPtr value = nullptr) : value(std::move(value)) {} explicit ReturnStmt(ExprPtr value = nullptr) : value(std::move(value)) {}
}; };
struct BreakStmt : public Stmt {
BreakStmt() = default;
};
struct ContinueStmt : public Stmt {
ContinueStmt() = default;
};
struct FunctionDecl : public Stmt { struct FunctionDecl : public Stmt {
std::string name; std::string name;
std::vector<std::pair<std::string, std::string>> parameters; // (type, name) std::vector<std::pair<std::string, std::string>> parameters; // (type, name)

View File

@@ -106,6 +106,10 @@ void Interpreter::execute_statement(const Stmt& stmt) {
exec_for(*for_stmt); exec_for(*for_stmt);
} else if (auto* return_stmt = dynamic_cast<const ReturnStmt*>(&stmt)) { } else if (auto* return_stmt = dynamic_cast<const ReturnStmt*>(&stmt)) {
exec_return(*return_stmt); exec_return(*return_stmt);
} else if (auto* break_stmt = dynamic_cast<const BreakStmt*>(&stmt)) {
exec_break(*break_stmt);
} else if (auto* continue_stmt = dynamic_cast<const ContinueStmt*>(&stmt)) {
exec_continue(*continue_stmt);
} else if (auto* func_decl = dynamic_cast<const FunctionDecl*>(&stmt)) { } else if (auto* func_decl = dynamic_cast<const FunctionDecl*>(&stmt)) {
exec_function_decl(*func_decl); exec_function_decl(*func_decl);
} else if (auto* class_decl = dynamic_cast<const ClassDecl*>(&stmt)) { } else if (auto* class_decl = dynamic_cast<const ClassDecl*>(&stmt)) {
@@ -397,8 +401,17 @@ void Interpreter::exec_if(const IfStmt& stmt) {
} }
void Interpreter::exec_while(const WhileStmt& stmt) { void Interpreter::exec_while(const WhileStmt& stmt) {
try {
while (is_truthy(evaluate(*stmt.condition))) { while (is_truthy(evaluate(*stmt.condition))) {
try {
execute_statement(*stmt.body); execute_statement(*stmt.body);
} catch (const ContinueException&) {
// Continue: just proceed to the next iteration (check condition again)
continue;
}
}
} catch (const BreakException&) {
// Break: exit the loop
} }
} }
@@ -412,12 +425,18 @@ void Interpreter::exec_for(const ForStmt& stmt) {
} }
while (!stmt.condition || is_truthy(evaluate(*stmt.condition))) { while (!stmt.condition || is_truthy(evaluate(*stmt.condition))) {
try {
execute_statement(*stmt.body); execute_statement(*stmt.body);
} catch (const ContinueException&) {
// Continue: proceed to increment
}
if (stmt.increment) { if (stmt.increment) {
evaluate(*stmt.increment); evaluate(*stmt.increment);
} }
} }
} catch (const BreakException&) {
// Break: exit the loop
} catch (...) { } catch (...) {
environment = previous; environment = previous;
throw; throw;
@@ -431,6 +450,14 @@ void Interpreter::exec_return(const ReturnStmt& stmt) {
throw ReturnException(value); throw ReturnException(value);
} }
void Interpreter::exec_break(const BreakStmt& stmt) {
throw BreakException();
}
void Interpreter::exec_continue(const ContinueStmt& stmt) {
throw ContinueException();
}
void Interpreter::exec_function_decl(const FunctionDecl& stmt) { void Interpreter::exec_function_decl(const FunctionDecl& stmt) {
auto func_decl = std::make_shared<FunctionDecl>(stmt); auto func_decl = std::make_shared<FunctionDecl>(stmt);
auto func = std::make_shared<FunctionValue>(stmt.name, func_decl); auto func = std::make_shared<FunctionValue>(stmt.name, func_decl);

View File

@@ -20,6 +20,9 @@ public:
explicit ReturnException(ValuePtr value) : value(std::move(value)) {} explicit ReturnException(ValuePtr value) : value(std::move(value)) {}
}; };
class BreakException : public std::exception {};
class ContinueException : public std::exception {};
class Environment { class Environment {
public: public:
std::shared_ptr<Environment> parent; std::shared_ptr<Environment> parent;
@@ -138,6 +141,8 @@ private:
void exec_while(const WhileStmt& stmt); void exec_while(const WhileStmt& stmt);
void exec_for(const ForStmt& stmt); void exec_for(const ForStmt& stmt);
void exec_return(const ReturnStmt& stmt); void exec_return(const ReturnStmt& stmt);
void exec_break(const BreakStmt& stmt);
void exec_continue(const ContinueStmt& stmt);
void exec_function_decl(const FunctionDecl& stmt); void exec_function_decl(const FunctionDecl& stmt);
void exec_class_decl(const ClassDecl& stmt); void exec_class_decl(const ClassDecl& stmt);

View File

@@ -61,7 +61,8 @@ void Lexer::skip_whitespace() {
} }
void Lexer::skip_comment() { void Lexer::skip_comment() {
if (peek() == '/' && peek_next() == '/') { if (peek() == '/') {
advance();
while (peek() != '\n' && !is_at_end()) { while (peek() != '\n' && !is_at_end()) {
advance(); advance();
} }
@@ -223,6 +224,8 @@ TokenType Lexer::get_keyword_type(const std::string& text) const {
{"and", TokenType::AND}, {"and", TokenType::AND},
{"or", TokenType::OR}, {"or", TokenType::OR},
{"this", TokenType::THIS}, {"this", TokenType::THIS},
{"continue", TokenType::CONTINUE},
{"break", TokenType::BREAK},
}; };
auto it = keywords.find(text); auto it = keywords.find(text);

View File

@@ -12,7 +12,7 @@ enum class TokenType {
// Keywords // Keywords
CLASS, FUNC, NUMBER, STRING, BOOL, LIST, MAP, CLASS, FUNC, NUMBER, STRING, BOOL, LIST, MAP,
IF, ELSE, WHILE, FOR, RETURN, VAR, IF, ELSE, WHILE, FOR, RETURN, VAR,
TRUE, FALSE, NIL, THIS, TRUE, FALSE, NIL, THIS, CONTINUE, BREAK,
// Operators // Operators
PLUS, MINUS, STAR, SLASH, PERCENT, PLUS, MINUS, STAR, SLASH, PERCENT,

View File

@@ -1,5 +1,6 @@
#include "parser.h" #include "parser.h"
#include <format> #include <format>
#include <iostream>
namespace camellya { namespace camellya {
@@ -12,6 +13,7 @@ Program Parser::parse() {
try { try {
statements.push_back(declaration()); statements.push_back(declaration());
} catch (const ParseError& error) { } catch (const ParseError& error) {
std::cerr << error.what() << std::endl;
synchronize(); synchronize();
} }
} }
@@ -118,7 +120,6 @@ StmtPtr Parser::class_declaration() {
members.push_back(function_declaration()); members.push_back(function_declaration());
} else { } else {
members.push_back(var_declaration()); members.push_back(var_declaration());
consume(TokenType::SEMICOLON, "Expected ';' after field declaration.");
} }
} }
@@ -168,6 +169,7 @@ StmtPtr Parser::var_declaration() {
if (match({TokenType::EQUAL})) { if (match({TokenType::EQUAL})) {
initializer = expression(); initializer = expression();
} }
consume(TokenType::SEMICOLON, "Expected ';' after variable declaration.");
return std::make_unique<VarDecl>(type_name, name.lexeme, std::move(initializer)); return std::make_unique<VarDecl>(type_name, name.lexeme, std::move(initializer));
} }
@@ -177,6 +179,8 @@ StmtPtr Parser::statement() {
if (match({TokenType::WHILE})) return while_statement(); if (match({TokenType::WHILE})) return while_statement();
if (match({TokenType::FOR})) return for_statement(); if (match({TokenType::FOR})) return for_statement();
if (match({TokenType::RETURN})) return return_statement(); if (match({TokenType::RETURN})) return return_statement();
if (match({TokenType::BREAK})) return break_statement();
if (match({TokenType::CONTINUE})) return continue_statement();
if (match({TokenType::LEFT_BRACE})) return block_statement(); if (match({TokenType::LEFT_BRACE})) return block_statement();
return expression_statement(); return expression_statement();
@@ -214,7 +218,6 @@ StmtPtr Parser::for_statement() {
StmtPtr initializer = nullptr; StmtPtr initializer = nullptr;
if (!match({TokenType::SEMICOLON})) { if (!match({TokenType::SEMICOLON})) {
initializer = declaration(); initializer = declaration();
consume(TokenType::SEMICOLON, "Expected ';' after for initializer.");
} }
ExprPtr condition = nullptr; ExprPtr condition = nullptr;
@@ -246,16 +249,21 @@ StmtPtr Parser::return_statement() {
return std::make_unique<ReturnStmt>(std::move(value)); return std::make_unique<ReturnStmt>(std::move(value));
} }
StmtPtr Parser::break_statement() {
consume(TokenType::SEMICOLON, "Expected ';' after 'break'.");
return std::make_unique<BreakStmt>();
}
StmtPtr Parser::continue_statement() {
consume(TokenType::SEMICOLON, "Expected ';' after 'continue'.");
return std::make_unique<ContinueStmt>();
}
StmtPtr Parser::block_statement() { StmtPtr Parser::block_statement() {
std::vector<StmtPtr> statements; std::vector<StmtPtr> statements;
while (!check(TokenType::RIGHT_BRACE) && !is_at_end()) { while (!check(TokenType::RIGHT_BRACE) && !is_at_end()) {
auto stmt = declaration(); statements.push_back(declaration());
// If declaration returned a VarDecl (not a class/function/statement), consume semicolon
if (dynamic_cast<VarDecl*>(stmt.get())) {
consume(TokenType::SEMICOLON, "Expected ';' after variable declaration.");
}
statements.push_back(std::move(stmt));
} }
consume(TokenType::RIGHT_BRACE, "Expected '}' after block."); consume(TokenType::RIGHT_BRACE, "Expected '}' after block.");

View File

@@ -44,6 +44,8 @@ private:
StmtPtr while_statement(); StmtPtr while_statement();
StmtPtr for_statement(); StmtPtr for_statement();
StmtPtr return_statement(); StmtPtr return_statement();
StmtPtr break_statement();
StmtPtr continue_statement();
StmtPtr block_statement(); StmtPtr block_statement();
StmtPtr expression_statement(); StmtPtr expression_statement();

View File

@@ -1,6 +1,6 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <catch2/benchmark/catch_benchmark.hpp> #include <catch2/benchmark/catch_benchmark.hpp>
#include "library.h" #include "camellya.h"
#include <memory> #include <memory>
@@ -148,3 +148,72 @@ TEST_CASE("interpreter performance: simple loop", "[perf][script]") {
REQUIRE(r_num->value == 499500.0); REQUIRE(r_num->value == 499500.0);
}; };
} }
TEST_CASE("loop break", "[script][loop]") {
State state;
const char* script = R"(
number sum = 0;
for (number i = 0; i < 10; i = i + 1) {
if (i == 5) {
break;
}
sum = sum + i;
}
)";
REQUIRE(state.do_string(script));
auto sum_val = state.get_global("sum");
REQUIRE(sum_val);
auto sum_num = std::dynamic_pointer_cast<NumberValue>(sum_val);
REQUIRE(sum_num->value == 10.0); // 0+1+2+3+4 = 10
}
TEST_CASE("loop continue", "[script][loop]") {
State state;
const char* script = R"(
number sum = 0;
for (number i = 0; i < 5; i = i + 1) {
if (i == 2) {
continue;
}
sum = sum + i;
}
)";
REQUIRE(state.do_string(script));
auto sum_val = state.get_global("sum");
REQUIRE(sum_val);
auto sum_num = std::dynamic_pointer_cast<NumberValue>(sum_val);
REQUIRE(sum_num->value == 8.0); // 0+1+3+4 = 8
}
TEST_CASE("while break and continue", "[script][loop]") {
State state;
const char* script = R"(
number i = 0;
number sum = 0;
while (i < 10) {
i = i + 1;
if (i == 3) {
continue;
}
if (i == 6) {
break;
}
sum = sum + i;
}
)";
REQUIRE(state.do_string(script));
auto sum_val = state.get_global("sum");
REQUIRE(sum_val);
auto sum_num = std::dynamic_pointer_cast<NumberValue>(sum_val);
REQUIRE(sum_num->value == 12.0);
// 1st iter: i=1, sum=1
// 2nd iter: i=2, sum=1+2=3
// 3rd iter: i=3, continue
// 4th iter: i=4, sum=3+4=7
// 5th iter: i=5, sum=7+5=12
// 6th iter: i=6, break
// Result should be 12.0
}