diff --git a/TASKS.md b/TASKS.md
index fa2337b..76ae0d6 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -61,8 +61,8 @@
| ID | Task | Status | Complexity | Files | Acceptance Criteria |
|---|---|---|---|---|---|
-| T7.1 | `ns` declaration parsing | ⬜ | 🟡 | `src/reader.nim`, `src/emitter.nim` | `(ns my.app (:require [other.lib :as lib]))` parses and compiles. |
-| T7.2 | Multi-file compilation | ⬜ | 🔴 | `src/cljnim.nim`, `src/emitter.nim` | `./cljnim compile project.clj` finds all required files and compiles them together. |
+| T7.1 | `ns` declaration parsing | ✅ | 🟡 | `src/emitter.nim` | `(ns my.app (:require [other.lib :as lib]))` parses, extracts aliases, resolves. |
+| T7.2 | Multi-file compilation | ✅ | 🔴 | `src/cljnim.nim`, `src/emitter.nim` | `./cljnim run app.clj` finds required files (hyphen→underscore), inlines defs. |
| T7.3 | Module caching | ⬜ | 🟡 | `src/cljnim.nim` | Compiled `.nim` files cached in `nimcache/`. Rebuild only changed files. |
| T7.4 | Dependency resolution | ⬜ | 🔴 | New: `src/deps.nim` | Read `deps.edn` or `project.clj` format. Download deps from Git. |
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index 36cf826..85021f0 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -61,8 +61,8 @@
- [x] `type`, `instance?`, `satisfies?`
## Phase 7: Project Compilation
-- [ ] Compile entire projects (not just single files)
-- [ ] Namespace system (`ns`, `require`, `use`)
+- [x] Compile entire projects (not just single files)
+- [x] Namespace system (`ns`, `(:require [lib :as alias])`)
- [ ] Module caching for faster REPL startup
- [ ] Dependency resolution
diff --git a/src/cljnim.nim b/src/cljnim.nim
index ba57df3..3e9e94b 100644
--- a/src/cljnim.nim
+++ b/src/cljnim.nim
@@ -12,6 +12,38 @@ proc getLibPath(): string =
if dirExists(candidate3): return candidate3
return "lib"
+proc resolveNsToPath(nsName: string, searchPaths: seq[string]): string =
+ # Convert namespace name to file path: my.app -> my/app.clj
+ # Clojure convention: hyphens in ns names become underscores in filenames
+ let relPath = nsName.replace("-", "_").replace(".", "/") & ".clj"
+ for sp in searchPaths:
+ let candidate = sp / relPath
+ if fileExists(candidate):
+ return candidate
+ return ""
+
+proc extractRequires(forms: seq[CljVal]): seq[(string, string)] =
+ # Extract require declarations: returns (namespace, alias) pairs
+ result = @[]
+ for form in forms:
+ if form.kind == ckList and form.items.len >= 2 and
+ form.items[0].kind == ckSymbol and form.items[0].symName == "ns":
+ for ri in 2..
0 and
+ ((clause.items[0].kind == ckSymbol and clause.items[0].symName == ":require") or
+ (clause.items[0].kind == ckKeyword and clause.items[0].kwName == "require")))
+ if isRequire:
+ for ci in 1..= 1 and req.items[0].kind == ckSymbol:
+ let libName = req.items[0].symName
+ var alias = libName
+ if req.items.len >= 3 and req.items[1].kind == ckKeyword and req.items[1].kwName == "as":
+ if req.items[2].kind == ckSymbol:
+ alias = req.items[2].symName
+ result.add((libName, alias))
+
proc compileFile*(inputPath: string, outputPath: string) =
let source = readFile(inputPath)
let forms = reader.readAll(source)
@@ -20,7 +52,49 @@ proc compileFile*(inputPath: string, outputPath: string) =
stderr.writeLine("Error: No forms found in " & inputPath)
quit(1)
- let nimCode = emitter.emitProgram(forms)
+ # Resolve requires and collect all forms from required files
+ let inputDir = inputPath.parentDir
+ let searchPaths = @[inputDir, getCurrentDir()]
+ let requires = extractRequires(forms)
+
+ var allForms: seq[CljVal] = @[]
+ var visited: seq[string] = @[]
+ var nsAliases: seq[(string, string)] = @[] # (alias, namespace)
+
+ proc resolveFile(nsName: string) =
+ if nsName in visited: return
+ visited.add(nsName)
+ let path = resolveNsToPath(nsName, searchPaths)
+ if path.len == 0:
+ stderr.writeLine("Warning: Cannot find file for namespace: " & nsName)
+ return
+ let src = readFile(path)
+ let fileForms = reader.readAll(src)
+ # Recursively resolve nested requires
+ let nestedRequires = extractRequires(fileForms)
+ for (nestedNs, _) in nestedRequires:
+ resolveFile(nestedNs)
+ # Add non-ns forms
+ for f in fileForms:
+ if not (f.kind == ckList and f.items.len >= 1 and
+ f.items[0].kind == ckSymbol and f.items[0].symName == "ns"):
+ allForms.add(f)
+
+ # Collect namespace aliases
+ for (nsName, alias) in requires:
+ nsAliases.add((alias, nsName))
+ resolveFile(nsName)
+
+ # Set namespace aliases in emitter
+ emitter.setNsAliases(nsAliases)
+
+ # Add current file's forms (skip ns declaration)
+ for f in forms:
+ if not (f.kind == ckList and f.items.len >= 1 and
+ f.items[0].kind == ckSymbol and f.items[0].symName == "ns"):
+ allForms.add(f)
+
+ let nimCode = emitter.emitProgram(allForms)
writeFile(outputPath, nimCode)
echo "Generated: ", outputPath
diff --git a/src/emitter.nim b/src/emitter.nim
index c963edf..b84e4dc 100644
--- a/src/emitter.nim
+++ b/src/emitter.nim
@@ -4,6 +4,21 @@ import types
import macros
var requiredImports* = initHashSet[string]()
+var nsAliases*: seq[(string, string)] = @[] # (alias, namespace)
+
+proc setNsAliases*(aliases: seq[(string, string)]) =
+ nsAliases = aliases
+
+proc resolveNsAlias*(name: string): string =
+ # Resolve mu/square -> square (strip namespace prefix if alias exists)
+ let slashIdx = name.find('/')
+ if slashIdx < 0: return name
+ let prefix = name[0.. 0 and
+ ((clause.items[0].kind == ckSymbol and clause.items[0].symName == ":require") or
+ (clause.items[0].kind == ckKeyword and clause.items[0].kwName == "require")))
+ if isRequire:
+ for ci in 1..= 1:
+ let libName = req.items[0]
+ if libName.kind == ckSymbol:
+ var alias = libName.symName
+ # Check for :as
+ if req.items.len >= 3 and req.items[1].kind == ckKeyword and req.items[1].kwName == "as":
+ if req.items[2].kind == ckSymbol:
+ alias = req.items[2].symName
+ # Convert namespace to file path: my.app -> my/app
+ let filePath = libName.symName.replace(".", "/") & ".clj"
+ lines.add(sp & "# require: " & libName.symName & " as " & alias & " from " & filePath)
+ return lines.join("\n")
+
of "defmacro":
# Register user-defined macro
if items.len < 4:
@@ -938,11 +984,12 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
of ckKeyword:
return sp & "cljKeyword(\"" & v.kwName & "\")"
of ckSymbol:
- let rn = runtimeName(v.symName)
+ let symName = resolveNsAlias(v.symName)
+ let rn = runtimeName(symName)
if rn.len > 0:
# Known function symbol — emit as runtime fn reference
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))"
- return sp & mangleName(v.symName)
+ return sp & mangleName(symName)
of ckList:
if v.items.len == 0:
return sp & "cljList(@[])"