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(@[])"