Skip to content

Commit 49b5d23

Browse files
committed
Add simple module system
`module` declaration with `(export ...)` for specifying public symbols `import` with optional selective symbol list Qualified names: `module/function` Private by default (only exported names are visible externally)
1 parent 21f1267 commit 49b5d23

File tree

3 files changed

+342
-9
lines changed

3 files changed

+342
-9
lines changed

src/compiler.rs

Lines changed: 189 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,8 @@ impl Compiler {
697697
"let" => return self.compile_let(&items[1..], dest, tail_pos),
698698
"fn" | "lambda" => return self.compile_fn(&items[1..], dest),
699699
"do" => return self.compile_do(&items[1..], dest, tail_pos),
700+
"module" => return self.compile_module(&items[1..], dest),
701+
"import" => return self.compile_import(&items[1..], dest),
700702
_ => {}
701703
}
702704
}
@@ -1248,6 +1250,181 @@ impl Compiler {
12481250
self.compile_expr(&args[args.len() - 1], dest, tail_pos)
12491251
}
12501252

1253+
/// Compile a module declaration:
1254+
/// (module name (export sym1 sym2 ...) body...)
1255+
///
1256+
/// This creates a module with the specified exports and stores it as a global.
1257+
/// Definitions inside the module body are stored as qualified globals (module/name).
1258+
fn compile_module(&mut self, args: &[Value], dest: Reg) -> Result<(), String> {
1259+
if args.len() < 2 {
1260+
return Err("module expects at least 2 arguments: name and (export ...)".to_string());
1261+
}
1262+
1263+
// Parse module name
1264+
let module_name = args[0]
1265+
.as_symbol()
1266+
.ok_or("module name must be a symbol")?;
1267+
1268+
// Parse export list: (export sym1 sym2 ...)
1269+
let export_list = args[1]
1270+
.as_list()
1271+
.ok_or("module expects (export ...) as second argument")?;
1272+
1273+
if export_list.is_empty() || export_list[0].as_symbol() != Some("export") {
1274+
return Err("module expects (export sym1 sym2 ...) as second argument".to_string());
1275+
}
1276+
1277+
let exports: Vec<String> = export_list[1..]
1278+
.iter()
1279+
.map(|v| {
1280+
v.as_symbol()
1281+
.ok_or_else(|| "export list must contain symbols".to_string())
1282+
.map(|s| s.to_string())
1283+
})
1284+
.collect::<Result<Vec<_>, _>>()?;
1285+
1286+
// Body expressions (definitions)
1287+
let body = &args[2..];
1288+
1289+
// We'll build the module by:
1290+
// 1. Compiling each body expression
1291+
// 2. For each def, we store as qualified global (module/name)
1292+
// 3. At the end, we create a Module value and store it
1293+
1294+
// Compile body expressions - definitions will be stored as module/name globals
1295+
// We need to track definitions made in this module
1296+
let mut defined_names: Vec<String> = Vec::new();
1297+
1298+
for expr in body {
1299+
// Check if this is a def expression to track the name
1300+
if let Some(items) = expr.as_list() {
1301+
if !items.is_empty() {
1302+
if let Some("def") = items[0].as_symbol() {
1303+
if items.len() >= 2 {
1304+
if let Some(name) = items[1].as_symbol() {
1305+
defined_names.push(name.to_string());
1306+
}
1307+
}
1308+
}
1309+
}
1310+
}
1311+
1312+
// Create a transformed expression with qualified name for def
1313+
let transformed = self.transform_module_def(expr, module_name)?;
1314+
let temp = self.alloc_reg();
1315+
self.compile_expr(&transformed, temp, false)?;
1316+
self.free_reg();
1317+
}
1318+
1319+
// Now create the Module object
1320+
// First, load all arguments
1321+
1322+
// Module name as string
1323+
let name_idx = self.add_constant(Value::string(module_name));
1324+
let name_reg = self.alloc_reg();
1325+
self.emit(Op::load_const(name_reg, name_idx));
1326+
1327+
// Export names as strings
1328+
for export_name in &exports {
1329+
let export_idx = self.add_constant(Value::string(export_name));
1330+
let export_reg = self.alloc_reg();
1331+
self.emit(Op::load_const(export_reg, export_idx));
1332+
}
1333+
1334+
// Call __make_module__
1335+
let fn_name_idx = self.add_constant(Value::symbol("__make_module__"));
1336+
if fn_name_idx <= 255 {
1337+
self.emit(Op::call_global(dest, fn_name_idx as u8, (1 + exports.len()) as u8));
1338+
} else {
1339+
return Err("Too many constants".to_string());
1340+
}
1341+
1342+
// Free temp registers
1343+
for _ in 0..=exports.len() {
1344+
self.free_reg();
1345+
}
1346+
1347+
// Store the module as a global
1348+
let module_global_idx = self.add_constant(Value::symbol(module_name));
1349+
self.emit(Op::set_global(module_global_idx, dest));
1350+
1351+
Ok(())
1352+
}
1353+
1354+
/// Transform a def expression inside a module to use qualified name
1355+
fn transform_module_def(&self, expr: &Value, module_name: &str) -> Result<Value, String> {
1356+
if let Some(items) = expr.as_list() {
1357+
if !items.is_empty() {
1358+
if let Some("def") = items[0].as_symbol() {
1359+
if items.len() >= 2 {
1360+
if let Some(name) = items[1].as_symbol() {
1361+
// Transform (def name value) to (def module/name value)
1362+
let qualified_name = format!("{}/{}", module_name, name);
1363+
let mut new_items = vec![items[0].clone(), Value::symbol(&qualified_name)];
1364+
new_items.extend(items[2..].iter().cloned());
1365+
return Ok(Value::list(new_items));
1366+
}
1367+
}
1368+
}
1369+
}
1370+
}
1371+
// Not a def or malformed - return as-is
1372+
Ok(expr.clone())
1373+
}
1374+
1375+
/// Compile an import statement:
1376+
/// (import module) - import all exports, access as module/name
1377+
/// (import module (sym1 sym2)) - import specific symbols into local scope
1378+
fn compile_import(&mut self, args: &[Value], dest: Reg) -> Result<(), String> {
1379+
if args.is_empty() {
1380+
return Err("import expects at least 1 argument".to_string());
1381+
}
1382+
1383+
let module_name = args[0]
1384+
.as_symbol()
1385+
.ok_or("import expects a module name symbol")?;
1386+
1387+
if args.len() == 1 {
1388+
// (import module) - just load the module to verify it exists
1389+
// The qualified names (module/name) are already available as globals
1390+
let module_idx = self.add_constant(Value::symbol(module_name));
1391+
self.emit(Op::get_global(dest, module_idx));
1392+
// Result is the module value (for chaining or inspection)
1393+
} else if args.len() == 2 {
1394+
// (import module (sym1 sym2 ...)) - import specific symbols
1395+
let symbols = args[1]
1396+
.as_list()
1397+
.ok_or("import expects a list of symbols as second argument")?;
1398+
1399+
for sym in symbols {
1400+
let sym_name = sym
1401+
.as_symbol()
1402+
.ok_or("import symbol list must contain symbols")?;
1403+
1404+
// Get the qualified name from the module
1405+
let qualified_name = format!("{}/{}", module_name, sym_name);
1406+
let qualified_idx = self.add_constant(Value::symbol(&qualified_name));
1407+
1408+
// Load the value
1409+
let temp = self.alloc_reg();
1410+
self.emit(Op::get_global(temp, qualified_idx));
1411+
1412+
// Store as unqualified global
1413+
let unqualified_idx = self.add_constant(Value::symbol(sym_name));
1414+
self.emit(Op::set_global(unqualified_idx, temp));
1415+
1416+
self.free_reg();
1417+
}
1418+
1419+
// Return nil
1420+
self.emit(Op::load_nil(dest));
1421+
} else {
1422+
return Err("import expects 1 or 2 arguments".to_string());
1423+
}
1424+
1425+
Ok(())
1426+
}
1427+
12511428
/// Try to compile a binary operation using specialized opcodes
12521429
/// Returns Some(true) if compiled, Some(false) if not applicable, Err on error
12531430
fn try_compile_binary_op(&mut self, op: &str, args: &[Value], dest: Reg) -> Result<Option<bool>, String> {
@@ -1414,11 +1591,15 @@ impl Compiler {
14141591

14151592
fn compile_call(&mut self, items: &[Value], dest: Reg, tail_pos: bool) -> Result<(), String> {
14161593
// Try constant folding for the entire call expression (including pure user functions)
1417-
let call_expr = Value::list(items.to_vec());
1418-
if let Some(folded) = try_const_eval_with_fns(&call_expr, &self.pure_fns) {
1419-
let idx = self.add_constant(folded);
1420-
self.emit(Op::load_const(dest, idx));
1421-
return Ok(());
1594+
// NOTE: Don't constant-fold qualified names (module/function) to preserve module privacy
1595+
let is_module_call = items[0].as_symbol().map_or(false, |s| s.contains('/'));
1596+
if !is_module_call {
1597+
let call_expr = Value::list(items.to_vec());
1598+
if let Some(folded) = try_const_eval_with_fns(&call_expr, &self.pure_fns) {
1599+
let idx = self.add_constant(folded);
1600+
self.emit(Op::load_const(dest, idx));
1601+
return Ok(());
1602+
}
14221603
}
14231604

14241605
// Try to compile as specialized binary operation
@@ -1465,6 +1646,8 @@ impl Compiler {
14651646
// Try inlining small non-recursive functions
14661647
// This eliminates call overhead for thin wrappers like:
14671648
// (def reverse (fn (lst) (reverse-acc lst (list))))
1649+
// NOTE: Don't inline qualified names (module/function) to preserve module privacy
1650+
if !op.contains('/') {
14681651
if let Some(fn_def) = self.inline_candidates.get(op).cloned() {
14691652
// Check: correct number of arguments
14701653
if args.len() == fn_def.params.len() {
@@ -1486,6 +1669,7 @@ impl Compiler {
14861669
}
14871670
}
14881671
}
1672+
} // end !op.contains('/')
14891673
}
14901674

14911675
// Check if calling a global symbol (optimization: use CallGlobal/TailCallGlobal)

src/value.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use crate::bytecode::Chunk;
1414
//
1515
// Arena allocation eliminates per-object reference counting overhead by:
1616
// 1. Allocating objects in a growable Vec using bump allocation
17-
// 2. Using indices instead of pointers for stable references
18-
// 3. Cloning is free (just copy the index)
17+
// 2. Using indices instead of pointers for references
18+
// 3. Cloning is free
1919
// 4. Dropping is a no-op (arena is cleared in bulk)
2020
// 5. Values that escape (returned from top-level) are promoted to Rc
2121
//
@@ -274,6 +274,16 @@ pub enum HeapObject {
274274
/// Atomic reference for shared mutable state
275275
/// Wrapped in Arc<Mutex<>> to allow thread-safe mutation
276276
Atom(Arc<Mutex<SharedValue>>),
277+
/// Module - a namespace containing exported definitions
278+
Module(Box<ModuleObject>),
279+
}
280+
281+
#[derive(Debug, Clone)]
282+
pub struct ModuleObject {
283+
/// Module name (e.g., "math")
284+
pub name: String,
285+
/// Exported symbols mapped to their values
286+
pub exports: HashMap<String, Value>,
277287
}
278288

279289
impl Clone for HeapObject {
@@ -290,6 +300,7 @@ impl Clone for HeapObject {
290300
HeapObject::ChannelSender(s) => HeapObject::ChannelSender(s.clone()),
291301
HeapObject::ChannelReceiver(r) => HeapObject::ChannelReceiver(r.clone()),
292302
HeapObject::Atom(a) => HeapObject::Atom(a.clone()),
303+
HeapObject::Module(m) => HeapObject::Module(m.clone()),
293304
}
294305
}
295306
}
@@ -509,6 +520,14 @@ impl Value {
509520
Value::from_heap(heap)
510521
}
511522

523+
pub fn module(name: &str, exports: HashMap<String, Value>) -> Value {
524+
let heap = Rc::new(HeapObject::Module(Box::new(ModuleObject {
525+
name: name.to_string(),
526+
exports,
527+
})));
528+
Value::from_heap(heap)
529+
}
530+
512531
/// Create a heap-allocated value from an Rc<HeapObject>
513532
/// Uses Rc::into_raw to store the pointer - refcount is NOT decremented
514533
pub fn from_heap(heap: Rc<HeapObject>) -> Value {
@@ -697,6 +716,14 @@ impl Value {
697716
}
698717
}
699718

719+
/// Get as a module reference
720+
pub fn as_module(&self) -> Option<&ModuleObject> {
721+
match self.as_heap() {
722+
Some(HeapObject::Module(m)) => Some(m),
723+
_ => None,
724+
}
725+
}
726+
700727
// Lisp semantics
701728

702729
/// Check if a value is truthy
@@ -795,6 +822,7 @@ impl Value {
795822
HeapObject::ChannelSender(_) => "channel-sender",
796823
HeapObject::ChannelReceiver(_) => "channel-receiver",
797824
HeapObject::Atom(_) => "atom",
825+
HeapObject::Module(_) => "module",
798826
}
799827
} else {
800828
"unknown"
@@ -998,6 +1026,14 @@ impl Value {
9981026
let heap = Rc::new(HeapObject::Atom(a.clone()));
9991027
Value::from_heap(heap)
10001028
}
1029+
Some(HeapObject::Module(m)) => {
1030+
// Promote module exports recursively
1031+
let promoted_exports: HashMap<String, Value> = m.exports
1032+
.iter()
1033+
.map(|(k, v)| (k.clone(), v.promote()))
1034+
.collect();
1035+
Value::module(&m.name, promoted_exports)
1036+
}
10011037
None => Value::nil(),
10021038
}
10031039
} else if self.is_ptr() {
@@ -1124,6 +1160,9 @@ impl Value {
11241160
Some(HeapObject::Atom(_)) => {
11251161
Err("Atoms cannot be converted to SharedValue (they are already thread-safe)".to_string())
11261162
}
1163+
Some(HeapObject::Module(_)) => {
1164+
Err("Modules cannot be shared across threads".to_string())
1165+
}
11271166
None => Err("Cannot convert unknown value to SharedValue".to_string()),
11281167
}
11291168
}
@@ -1270,6 +1309,7 @@ impl fmt::Display for Value {
12701309
let value = a.lock().unwrap();
12711310
write!(f, "(atom {})", value)
12721311
}
1312+
HeapObject::Module(m) => write!(f, "<module {}>", m.name),
12731313
}
12741314
} else {
12751315
write!(f, "<unknown>")

0 commit comments

Comments
 (0)