Skip to content

Commit

Permalink
Merge pull request #213 from KennyOliver/issue-211
Browse files Browse the repository at this point in the history
Issue 211: Add Import/Export Support
  • Loading branch information
KennyOliver authored Jan 16, 2025
2 parents 216d60d + 4a6a6ab commit 8d232f8
Show file tree
Hide file tree
Showing 19 changed files with 409 additions and 25 deletions.
32 changes: 31 additions & 1 deletion docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

---

Expand Down
148 changes: 148 additions & 0 deletions src/interpreter/interpreter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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);
}
5 changes: 5 additions & 0 deletions src/interpreter/interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "../shared/data_types.h"
#include "builtins.h"
#include "interpreter_types.h"
#include "module_cache.h"
#include "utils.h"
#include <errno.h>
#include <limits.h>
Expand Down Expand Up @@ -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 {
Expand All @@ -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
7 changes: 7 additions & 0 deletions src/interpreter/interpreter_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions src/interpreter/module_cache.c
Original file line number Diff line number Diff line change
@@ -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;
}
24 changes: 24 additions & 0 deletions src/interpreter/module_cache.h
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>

// 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
10 changes: 10 additions & 0 deletions src/interpreter/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions src/lexer/keywords.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down
Loading

0 comments on commit 8d232f8

Please sign in to comment.