Compare commits

...

7 Commits

Author SHA1 Message Date
4247a59146 Move readme 2026-01-16 00:02:08 +08:00
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
18 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,56 +1,78 @@
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()
include(FetchContent) target_include_directories(libcamellya PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
FetchContent_Declare( if(CAMELLYA_BUILD_CLI)
Catch2 add_executable(camellya
GIT_REPOSITORY https://github.com/catchorg/Catch2.git cli/main.cpp
GIT_TAG v3.6.0 )
) target_link_libraries(camellya
PRIVATE
libcamellya
)
endif()
FetchContent_MakeAvailable(Catch2) if(CAMELLYA_BUILD_TESTS)
include(FetchContent)
enable_testing() FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.12.0
)
add_executable(camellya_tests FetchContent_MakeAvailable(Catch2)
tests/test_basic.cpp
)
target_include_directories(camellya_tests enable_testing()
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(camellya_tests add_executable(camellya_tests
PRIVATE tests/test_basic.cpp
camellya )
Catch2::Catch2WithMain
)
add_test(NAME camellya_tests COMMAND camellya_tests) target_include_directories(camellya_tests
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(camellya_tests
PRIVATE
libcamellya
Catch2::Catch2WithMain
)
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) {
while (is_truthy(evaluate(*stmt.condition))) { try {
execute_statement(*stmt.body); while (is_truthy(evaluate(*stmt.condition))) {
try {
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))) {
execute_statement(*stmt.body); try {
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
}