diff --git a/docs/tutorial.md b/docs/tutorial.md index f63c12f..6a9ccb6 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -16,6 +16,7 @@ Welcome to FlavorLang! This tutorial will guide you through the fundamentals of 8. [Working with Arrays](#working-with-arrays) 9. [File Operations](#file-operations) 10. [Standard Library Functions](#standard-library-functions) +11. [Imports & Exports](#imports-and-exports) ## Getting Started @@ -261,9 +262,38 @@ let len = length("Hello"); # 5 sleep(1000); # Pause for 1 second ``` +## Modules: Imports & Exports + +FlavorLang supports modularity so you can split your code into separate files and only expose what’s necessary! + +### Exporting + +Use the `export` keyword to mark functions, variables, or constants as public. Items declared without `export` remain private to the file. + +```js +export create triple(x) { + deliver x * 3; +} + +create hiddenFunc() {} + +export let someVar = triple(5); +``` + +### Importing + +To use exported items in another file, use the `import` keyword at the beginning of the file. + +```py +import "24_export.flv"; + +serve(someVar); # Output: 15 +serve(hiddenFunc()); # This won't work! +``` + --- -This tutorial covers the main features of FlavorLang. You can now start creating your own programs using these cooking-inspired programming concepts! Remember that like cooking, programming gets better with practice, so don't be afraid to experiment with different combinations of these features. +This tutorial covers the main features of FlavorLang. You can now start creating your own programs using these culinary programming concepts! Remember that like cooking, programming gets better with practice, so don't be afraid to experiment with different combinations of these features. --- diff --git a/src/interpreter/interpreter.c b/src/interpreter/interpreter.c index 32cbc26..8028eb0 100644 --- a/src/interpreter/interpreter.c +++ b/src/interpreter/interpreter.c @@ -165,6 +165,16 @@ InterpretResult interpret_node(ASTNode *node, Environment *env) { result = interpret_array_slice_access(node, env); break; + case AST_IMPORT: + debug_print_int("\tMatched: `AST_IMPORT`\n"); + result = interpret_import(node, env); + break; + + case AST_EXPORT: + debug_print_int("\tMatched: `AST_EXPORT`\n"); + result = interpret_export(node, env); + break; + default: return raise_error("Unsupported `ASTNode` type.\n"); } @@ -2430,3 +2440,141 @@ InterpretResult interpret_array_slice_access(ASTNode *node, Environment *env) { return make_result(result, false, false); } } + +void merge_module_exports(Environment *dest_env, Environment *export_env) { + // Merge only those whose name is in `exported_symbols` + for (size_t j = 0; j < export_env->exported_count; j++) { + const char *exported_name = export_env->exported_symbols[j]; + + // Check if it's a variable in `export_env` + Variable *var = get_variable(export_env, exported_name); + if (var) { + add_variable(dest_env, *var); + } + + // Also check if it's a function in `export_env` + Function *fn = get_function(export_env, exported_name); + if (fn) { + // Only add if not already defined, etc + if (!get_function(dest_env, fn->name)) { + add_function(dest_env, *fn); + } + } + } +} + +void register_export(Environment *env, const char *symbol_name) { + debug_print_int("Registering exported symbol: %s\n", symbol_name); + + // If array is full, reallocate + if (env->exported_count == env->exported_capacity) { + size_t newcap = + env->exported_capacity == 0 ? 4 : env->exported_capacity * 2; + char **temp = realloc(env->exported_symbols, newcap * sizeof(char *)); + if (!temp) { + fatal_error("Memory allocation failed while registering export.\n"); + } + env->exported_symbols = temp; + env->exported_capacity = newcap; + } + + // Store a copy of the exported symbol's name + env->exported_symbols[env->exported_count++] = strdup(symbol_name); +} + +InterpretResult interpret_import(ASTNode *node, Environment *env) { + if (!node || node->type != AST_IMPORT) { + return raise_error( + "Internal error: invalid node passed to interpret_import.\n"); + } + + char *module_path = node->import.import_path; + if (!module_path) { + return raise_error("Module path is missing in import statement.\n"); + } + + char resolved_path[PATH_MAX]; + if (module_path[0] == '/') { + // It's already an absolute path + strncpy(resolved_path, module_path, PATH_MAX); + } else { + // It's relative + snprintf(resolved_path, PATH_MAX, "%s/%s", env->script_dir, + module_path); + } + + // Read file + char *source = read_file(resolved_path); + if (!source) { + return raise_error("Failed to read module file: %s\n", resolved_path); + } + + // Tokenize & parse module + Token *tokens = tokenize(source); + free(source); + if (!tokens) { + return raise_error("Tokenization failed for module file: %s\n", + module_path); + } + ASTNode *module_ast = parse_program(tokens); + free_token_array(tokens); + if (!module_ast) { + return raise_error("Parsing failed for module file: %s\n", module_path); + } + + // Create a new Environment for the module + // For isolation, make current Environment the parent + Environment module_env; + init_environment_with_parent(&module_env, env); + + // Interpret module + interpret_program(module_ast, &module_env); + free_ast(module_ast); + + // Store module's exported symbols in cache + Environment *export_env = malloc(sizeof(Environment)); + if (!export_env) { + fatal_error("Memory allocation failed while caching module exports.\n"); + } + + *export_env = module_env; + store_module_cache(module_path, export_env); + + // Merge exported symbols into current Environment + merge_module_exports(env, export_env); + + free_environment(&module_env); + + return make_result(create_default_value(), false, false); +} + +InterpretResult interpret_export(ASTNode *node, Environment *env) { + if (!node || node->type != AST_EXPORT) { + return raise_error( + "Internal error: invalid node passed to interpret_export.\n"); + } + + // Evaluate wrapped declaration + InterpretResult decl_res = interpret_node(node->export.decl, env); + if (decl_res.is_error) { + return decl_res; // propagate error + } + + switch (node->export.decl->type) { + case AST_VAR_DECLARATION: + register_export(env, node->export.decl->var_declaration.variable_name); + break; + case AST_CONST_DECLARATION: + register_export(env, + node->export.decl->const_declaration.constant_name); + break; + case AST_FUNCTION_DECLARATION: + register_export(env, node->export.decl->function_declaration.name); + break; + default: + fprintf(stderr, "Warning: Export is a non-declaration type"); + break; + } + + return make_result(decl_res.value, false, false); +} diff --git a/src/interpreter/interpreter.h b/src/interpreter/interpreter.h index 0c4d8f9..c77f3b1 100644 --- a/src/interpreter/interpreter.h +++ b/src/interpreter/interpreter.h @@ -5,6 +5,7 @@ #include "../shared/data_types.h" #include "builtins.h" #include "interpreter_types.h" +#include "module_cache.h" #include "utils.h" #include #include @@ -34,6 +35,8 @@ InterpretResult call_user_defined_function(Function *func_ref, ASTNode *call_node, Environment *env); InterpretResult interpret_try(ASTNode *node, Environment *env); +InterpretResult interpret_import(ASTNode *node, Environment *env); +InterpretResult interpret_export(ASTNode *node, Environment *env); // Arrays typedef struct { @@ -58,5 +61,7 @@ LiteralValue create_default_value(void); Variable *get_variable(Environment *env, const char *variable_name); InterpretResult add_variable(Environment *env, Variable var); ASTFunctionParameter *copy_function_parameters(ASTFunctionParameter *params); +void merge_module_exports(Environment *dest_env, Environment *export_env); +void register_export(Environment *env, const char *symbol_name); #endif diff --git a/src/interpreter/interpreter_types.h b/src/interpreter/interpreter_types.h index f02a327..87a355d 100644 --- a/src/interpreter/interpreter_types.h +++ b/src/interpreter/interpreter_types.h @@ -83,6 +83,13 @@ struct Environment { size_t function_capacity; Environment *parent; // Parent environment for nested scopes + + // For modules + char **exported_symbols; + size_t exported_count; + size_t exported_capacity; + + char *script_dir; // Store directory of main script }; // Structure for Interpret Results diff --git a/src/interpreter/module_cache.c b/src/interpreter/module_cache.c new file mode 100644 index 0000000..da32753 --- /dev/null +++ b/src/interpreter/module_cache.c @@ -0,0 +1,24 @@ +#include "module_cache.h" + +ModuleCacheEntry *lookup_module_cache(const char *module_path) { + ModuleCacheEntry *entry = moduleCacheHead; + while (entry) { + if (strcmp(entry->module_path, module_path) == 0) { + return entry; + } + entry = entry->next; + } + return NULL; +} + +void store_module_cache(const char *module_path, Environment *export_env) { + ModuleCacheEntry *entry = calloc(1, sizeof(ModuleCacheEntry)); + if (!entry) { + fatal_error("Memory allocation failed in store_module_cache.\n"); + } + entry->module_path = strdup(module_path); + entry->export_env = + export_env; // might want to copy export table in the future + entry->next = moduleCacheHead; + moduleCacheHead = entry; +} diff --git a/src/interpreter/module_cache.h b/src/interpreter/module_cache.h new file mode 100644 index 0000000..2c84e89 --- /dev/null +++ b/src/interpreter/module_cache.h @@ -0,0 +1,24 @@ +#ifndef MODULE_CACHE_H +#define MODULE_CACHE_H + +#include "../lexer/lexer.h" +#include "../parser/parser.h" +#include "interpreter_types.h" +#include "utils.h" +#include +#include +#include + +// Simple struct to hold cached module exports +typedef struct ModuleCacheEntry { + char *module_path; // key + Environment *export_env; // pointer to an environment holding exports + struct ModuleCacheEntry *next; +} ModuleCacheEntry; + +static ModuleCacheEntry *moduleCacheHead = NULL; + +ModuleCacheEntry *lookup_module_cache(const char *module_path); +void store_module_cache(const char *module_path, Environment *export_env); + +#endif diff --git a/src/interpreter/utils.c b/src/interpreter/utils.c index 1931f49..bf2fdb8 100644 --- a/src/interpreter/utils.c +++ b/src/interpreter/utils.c @@ -97,6 +97,11 @@ void init_environment(Environment *env) { fatal_error("Failed to allocate memory for functions.\n"); } + // Initialize exported symbols + env->exported_symbols = NULL; + env->exported_count = 0; + env->exported_capacity = 0; + // Initialize built-in functions ONLY for the GLOBAL environment initialize_all_builtin_functions(env); } @@ -121,6 +126,11 @@ void init_environment_with_parent(Environment *env, Environment *parent) { fatal_error("Failed to allocate memory for functions.\n"); } + // Initialize exported symbols + env->exported_symbols = NULL; + env->exported_count = 0; + env->exported_capacity = 0; + // Do NOT initialize built-in functions in local environments } diff --git a/src/lexer/keywords.c b/src/lexer/keywords.c index d5c4fc7..bb36473 100644 --- a/src/lexer/keywords.c +++ b/src/lexer/keywords.c @@ -25,6 +25,8 @@ const char *KEYWORDS[] = { "recipe", // import "True", // Boolean True "False", // Boolean False + "import", // Import `.flv` script + "export", // Export identifiers in `.flv` script NULL // sentinel value }; diff --git a/src/main.c b/src/main.c index 20c348c..57d978c 100644 --- a/src/main.c +++ b/src/main.c @@ -193,21 +193,22 @@ void print_about(void) { } int main(int argc, char **argv) { - // Handle --about flag separately - if (argc == 2 && strcmp(argv[1], "--about") == 0) { - print_about(); - return EXIT_SUCCESS; - } - - // Parse command-line arguments + // Handle --about flag, parse CLI args, etc. Options options; parse_cli_args(argc, argv, &options); - // Read source `.flv` file - char *source = read_file(options.filename); + // Convert to absolute path. + char absolute_path[PATH_MAX]; + if (!realpath(options.filename, absolute_path)) { + perror("Error obtaining absolute path"); + exit(EXIT_FAILURE); + } + + // Read source file using the absolute path. + char *source = read_file(absolute_path); if (!source) { - fprintf(stderr, "Error: Could not read file '%s'\n", options.filename); - return EXIT_FAILURE; + fprintf(stderr, "Error: Could not read file '%s'\n", absolute_path); + exit(EXIT_FAILURE); } // Tokenize @@ -215,23 +216,20 @@ int main(int argc, char **argv) { if (!tokens) { fprintf(stderr, "Error: Tokenization failed\n"); free(source); - return EXIT_FAILURE; + exit(EXIT_FAILURE); } - // If --minify flag is set, perform minification and exit + // Optional: If minify, perform minification. if (options.minify) { - char *minified_filename = generate_minified_filename(options.filename); + char *minified_filename = generate_minified_filename(absolute_path); minify_tokens(tokens, minified_filename); printf("Minified script written to '%s'\n", minified_filename); free(minified_filename); - - // Clean up memory free(tokens); free(source); return EXIT_SUCCESS; } - // If --debug flag is set, print debug information if (debug_flag) { debug_print_tokens(tokens); debug_print_basic("Lexing complete!\n\n"); @@ -245,10 +243,21 @@ int main(int argc, char **argv) { debug_print_basic("Finished printing AST\n\n"); } - // Create environment + // Create environment and store the script's directory. Environment env; init_environment(&env); + // Extract the directory part from the absolute path. + char path_copy[PATH_MAX]; + strncpy(path_copy, absolute_path, PATH_MAX); + path_copy[PATH_MAX - 1] = '\0'; // ensure null-termination + char *dir = dirname(path_copy); + env.script_dir = strdup(dir); // <-- New field in Environment + if (!env.script_dir) { + perror("Failed to allocate memory for script_dir"); + exit(EXIT_FAILURE); + } + // Interpret interpret_program(ast, &env); if (debug_flag) { diff --git a/src/main.h b/src/main.h index 8301390..8c5f524 100644 --- a/src/main.h +++ b/src/main.h @@ -5,6 +5,8 @@ #include "interpreter/interpreter.h" #include "lexer/lexer.h" #include "parser/parser.h" +#include // for `dirname()` +#include #include #include #include diff --git a/src/parser/parser.c b/src/parser/parser.c index ede98f8..7e776b0 100644 --- a/src/parser/parser.c +++ b/src/parser/parser.c @@ -53,6 +53,10 @@ ASTNode *parse_statement(ParserState *state) { return parse_function_return(state); if (match_token(state, "try")) return parse_try_block(state); + if (match_token(state, "import")) + return parse_import_statement(state); + if (match_token(state, "export")) + return parse_export_statement(state); // Handle function calls if (token->type == TOKEN_FUNCTION_NAME) { @@ -998,3 +1002,56 @@ bool is_assignment(ParserState *state) { return false; } + +ASTNode *parse_import_statement(ParserState *state) { + expect_token(state, TOKEN_KEYWORD, "Expected `import` keyword"); + + Token *path_token = get_current_token(state); + if (path_token->type != TOKEN_STRING) { + parser_error("Expected module path as string after import", path_token); + } + + ASTNode *node = calloc(1, sizeof(ASTNode)); + if (!node) { + parser_error("Memory allocation failed for import node", path_token); + } + + node->type = AST_IMPORT; + node->import.import_path = strdup(path_token->lexeme); + advance_token(state); + expect_token(state, TOKEN_DELIMITER, "Expected `;` after import statement"); + + return node; +} + +ASTNode *parse_export_statement(ParserState *state) { + expect_token(state, TOKEN_KEYWORD, "Expected `export` keyword"); + + Token *current = get_current_token(state); + ASTNode *decl = NULL; + + if (current->type == TOKEN_KEYWORD && strcmp(current->lexeme, "let") == 0) { + decl = parse_variable_declaration(state); + } else if (current->type == TOKEN_KEYWORD && + strcmp(current->lexeme, "const") == 0) { + decl = parse_constant_declaration(state); + } else if (current->type == TOKEN_KEYWORD && + strcmp(current->lexeme, "create") == 0) { + decl = parse_function_declaration(state); + } else { + parser_error("Expected `let`, `const`, or `create` after `export`", + current); + } + + // Wrap declaration in AST_EXPORT + ASTNode *node = calloc(1, sizeof(ASTNode)); + if (!node) { + parser_error("Memory allocation failed for export node", + get_current_token(state)); + } + node->type = AST_EXPORT; + node->export.decl = decl; + node->next = NULL; + + return node; +} diff --git a/src/parser/parser.h b/src/parser/parser.h index b08d3cb..e953102 100644 --- a/src/parser/parser.h +++ b/src/parser/parser.h @@ -29,6 +29,8 @@ ASTNode *parse_function_return(ParserState *state); ASTNode *parse_try_block(ParserState *state); ASTCatchNode *parse_catch_block(ParserState *state); ASTNode *parse_finally_block(ParserState *state); +ASTNode *parse_import_statement(ParserState *state); +ASTNode *parse_export_statement(ParserState *state); // Expression parsing ASTNode *parse_expression(ParserState *state); @@ -39,7 +41,6 @@ ASTNode *create_variable_reference_node(char *name); // Helper functions ASTNode *parse_declaration(ParserState *state, ASTNodeType type); -bool match_token(ParserState *state, const char *lexeme); Token *peek_next_token(ParserState *state); Token *peek_ahead(ParserState *state, size_t n); ASTNode *parse_expression_statement(ParserState *state); diff --git a/src/parser/parser_state.c b/src/parser/parser_state.c index 705f868..cd7bcfa 100644 --- a/src/parser/parser_state.c +++ b/src/parser/parser_state.c @@ -38,7 +38,9 @@ void expect_token(ParserState *state, TokenType expected, bool match_token(ParserState *state, const char *lexeme) { Token *token = get_current_token(state); - return token && strcmp(token->lexeme, lexeme) == 0; + if (!token || token->type == TOKEN_EOF) + return false; + return (token->lexeme && strcmp(token->lexeme, lexeme) == 0); } Token *peek_next_token(ParserState *state) { diff --git a/src/parser/utils.c b/src/parser/utils.c index 3994f00..0cc6a1b 100644 --- a/src/parser/utils.c +++ b/src/parser/utils.c @@ -6,7 +6,6 @@ void free_ast(ASTNode *node) { switch (node->type) { case AST_ASSIGNMENT: - // free(node->assignment.variable_name); free_ast(node->assignment.lhs); free_ast(node->assignment.rhs); break; @@ -154,6 +153,14 @@ void free_ast(ASTNode *node) { // No dynamic memory to free break; + case AST_IMPORT: + free(node->import.import_path); + break; + + case AST_EXPORT: + free_ast(node->export.decl); + break; + default: fprintf(stderr, "Unknown ASTNode type `%d` in free_ast.\n", node->type); @@ -473,6 +480,19 @@ void print_ast(ASTNode *node, int depth) { printf("Variable Reference: %s\n", node->variable_name); break; + case AST_IMPORT: + printf("Import Statement:\n"); + print_indent(depth + 1); + printf("Path: %s\n", node->import.import_path); + break; + + case AST_EXPORT: + printf("Export Statement:\n"); + print_indent(depth + 1); + printf("Exported Declaration:\n"); + print_ast(node->export.decl, depth + 2); + break; + default: printf("Unknown AST Node Type: %d\n", node->type); break; diff --git a/src/shared/ast_types.h b/src/shared/ast_types.h index b08881a..8bacc21 100644 --- a/src/shared/ast_types.h +++ b/src/shared/ast_types.h @@ -29,7 +29,9 @@ typedef enum { AST_ARRAY_OPERATION, AST_ARRAY_INDEX_ACCESS, AST_ARRAY_SLICE_ACCESS, - AST_VARIABLE_REFERENCE + AST_VARIABLE_REFERENCE, + AST_IMPORT, + AST_EXPORT } ASTNodeType; // Literal Node @@ -170,6 +172,13 @@ typedef struct { struct ASTNode *operand; // Element to operate with (e.g., to append) } ASTArrayOperation; +typedef struct { + char *import_path; // Module script file path string +} ASTImport; +typedef struct { + struct ASTNode *decl; // Declaration node that's being exported +} ASTExport; + // AST Node Structure typedef struct ASTNode { ASTNodeType type; @@ -218,6 +227,10 @@ typedef struct ASTNode { // Literal and Reference LiteralNode literal; char *variable_name; // For AST_VARIABLE_REFERENCE + + // Import & Export + ASTImport import; + ASTExport export; }; struct ASTNode *next; diff --git a/src/tests/24_export.flv b/src/tests/24_export.flv new file mode 100644 index 0000000..0c593db --- /dev/null +++ b/src/tests/24_export.flv @@ -0,0 +1,9 @@ +export create triple(x) { + deliver x * 3; +} + +create hiddenFunc() {} + +export let someVar = triple(5); +let hiddenVar = -1; +const hiddenConst = -2; diff --git a/src/tests/25_import.flv b/src/tests/25_import.flv new file mode 100644 index 0000000..b231ba3 --- /dev/null +++ b/src/tests/25_import.flv @@ -0,0 +1,21 @@ +import "24_export.flv"; + +serve(someVar); + +try { + serve(hiddenFunc()); +} rescue { + serve("Hidden function error caught!"); +} + +try { + serve(hiddenVar); +} rescue { + serve("Hidden variable error caught!"); +} + +try { + serve(hiddenConst); +} rescue { + serve("Hidden constant error caught!"); +} diff --git a/vscode-extension/package.json b/vscode-extension/package.json index f768179..7efe22d 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "flavorlang-vscode", "displayName": "FlavorLang Support", "description": "Syntax highlighting for FlavorLang programming language.", - "version": "1.3.0", + "version": "1.4.0", "publisher": "KennyOliver", "repository": { "type": "git", diff --git a/vscode-extension/syntaxes/flavorlang.tmLanguage.json b/vscode-extension/syntaxes/flavorlang.tmLanguage.json index 9859a4a..137f79f 100644 --- a/vscode-extension/syntaxes/flavorlang.tmLanguage.json +++ b/vscode-extension/syntaxes/flavorlang.tmLanguage.json @@ -40,7 +40,7 @@ "patterns": [ { "name": "keyword.control.flavorlang", - "match": "\\b(let|const|if|elif|else|for|in|while|create|burn|deliver|check|is|rescue|try|rescue|finish|break|continue)\\b" + "match": "\\b(let|const|if|elif|else|for|in|while|create|burn|deliver|check|is|rescue|try|rescue|finish|break|continue|import|export)\\b" }, { "name": "keyword.other.flavorlang",