Phase 7: ns declaration + multi-file compilation
T7.1: Namespace declaration parsing (ns my.app (:require [lib :as l])) with :require/:as support Hyphen-to-underscore filename convention (math-utils -> math_utils.clj) T7.2: Multi-file compilation Resolves requires, recursively loads dependencies Inlines all defs into single Nim file Namespace alias resolution (mu/square -> square) 86 tests pass, all examples work.
This commit is contained in:
@@ -61,8 +61,8 @@
|
|||||||
|
|
||||||
| ID | Task | Status | Complexity | Files | Acceptance Criteria |
|
| 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.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 compile project.clj` finds all required files and compiles them together. |
|
| 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.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. |
|
| T7.4 | Dependency resolution | ⬜ | 🔴 | New: `src/deps.nim` | Read `deps.edn` or `project.clj` format. Download deps from Git. |
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -61,8 +61,8 @@
|
|||||||
- [x] `type`, `instance?`, `satisfies?`
|
- [x] `type`, `instance?`, `satisfies?`
|
||||||
|
|
||||||
## Phase 7: Project Compilation
|
## Phase 7: Project Compilation
|
||||||
- [ ] Compile entire projects (not just single files)
|
- [x] Compile entire projects (not just single files)
|
||||||
- [ ] Namespace system (`ns`, `require`, `use`)
|
- [x] Namespace system (`ns`, `(:require [lib :as alias])`)
|
||||||
- [ ] Module caching for faster REPL startup
|
- [ ] Module caching for faster REPL startup
|
||||||
- [ ] Dependency resolution
|
- [ ] Dependency resolution
|
||||||
|
|
||||||
|
|||||||
+75
-1
@@ -12,6 +12,38 @@ proc getLibPath(): string =
|
|||||||
if dirExists(candidate3): return candidate3
|
if dirExists(candidate3): return candidate3
|
||||||
return "lib"
|
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..<form.items.len:
|
||||||
|
let clause = form.items[ri]
|
||||||
|
let isRequire = (clause.kind == ckList and clause.items.len > 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..<clause.items.len:
|
||||||
|
let req = clause.items[ci]
|
||||||
|
if req.kind == ckVector and req.items.len >= 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) =
|
proc compileFile*(inputPath: string, outputPath: string) =
|
||||||
let source = readFile(inputPath)
|
let source = readFile(inputPath)
|
||||||
let forms = reader.readAll(source)
|
let forms = reader.readAll(source)
|
||||||
@@ -20,7 +52,49 @@ proc compileFile*(inputPath: string, outputPath: string) =
|
|||||||
stderr.writeLine("Error: No forms found in " & inputPath)
|
stderr.writeLine("Error: No forms found in " & inputPath)
|
||||||
quit(1)
|
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)
|
writeFile(outputPath, nimCode)
|
||||||
echo "Generated: ", outputPath
|
echo "Generated: ", outputPath
|
||||||
|
|
||||||
|
|||||||
+50
-3
@@ -4,6 +4,21 @@ import types
|
|||||||
import macros
|
import macros
|
||||||
|
|
||||||
var requiredImports* = initHashSet[string]()
|
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..<slashIdx]
|
||||||
|
let suffix = name[slashIdx+1..^1]
|
||||||
|
for (alias, ns) in nsAliases:
|
||||||
|
if prefix == alias:
|
||||||
|
return suffix
|
||||||
|
return name
|
||||||
|
|
||||||
type
|
type
|
||||||
EmitterError* = object of CatchableError
|
EmitterError* = object of CatchableError
|
||||||
@@ -130,10 +145,41 @@ proc emitSpecialForm(items: seq[CljVal], indent: int): string =
|
|||||||
let head = items[0]
|
let head = items[0]
|
||||||
if head.kind != ckSymbol:
|
if head.kind != ckSymbol:
|
||||||
raise newException(EmitterError, "List head must be a symbol, got " & $head.kind)
|
raise newException(EmitterError, "List head must be a symbol, got " & $head.kind)
|
||||||
let op = head.symName
|
let op = resolveNsAlias(head.symName)
|
||||||
|
|
||||||
# ---- Special forms (language constructs) ----
|
# ---- Special forms (language constructs) ----
|
||||||
case op
|
case op
|
||||||
|
of "ns":
|
||||||
|
# Namespace declaration: (ns my.app (:require [other.lib :as lib]))
|
||||||
|
if items.len < 2:
|
||||||
|
raise newException(EmitterError, "ns requires a namespace name")
|
||||||
|
let nsName = items[1]
|
||||||
|
if nsName.kind != ckSymbol:
|
||||||
|
raise newException(EmitterError, "ns name must be a symbol")
|
||||||
|
var lines: seq[string] = @[]
|
||||||
|
lines.add(sp & "# Namespace: " & nsName.symName)
|
||||||
|
# Process require clauses
|
||||||
|
for ri in 2..<items.len:
|
||||||
|
let clause = items[ri]
|
||||||
|
let isRequire = (clause.kind == ckList and clause.items.len > 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..<clause.items.len:
|
||||||
|
let req = clause.items[ci]
|
||||||
|
if req.kind == ckVector and req.items.len >= 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":
|
of "defmacro":
|
||||||
# Register user-defined macro
|
# Register user-defined macro
|
||||||
if items.len < 4:
|
if items.len < 4:
|
||||||
@@ -938,11 +984,12 @@ proc emitExpr*(v: CljVal, indent: int = 0): string =
|
|||||||
of ckKeyword:
|
of ckKeyword:
|
||||||
return sp & "cljKeyword(\"" & v.kwName & "\")"
|
return sp & "cljKeyword(\"" & v.kwName & "\")"
|
||||||
of ckSymbol:
|
of ckSymbol:
|
||||||
let rn = runtimeName(v.symName)
|
let symName = resolveNsAlias(v.symName)
|
||||||
|
let rn = runtimeName(symName)
|
||||||
if rn.len > 0:
|
if rn.len > 0:
|
||||||
# Known function symbol — emit as runtime fn reference
|
# Known function symbol — emit as runtime fn reference
|
||||||
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))"
|
return sp & "cljFn(proc(args: seq[CljVal]): CljVal = " & rn & "(args))"
|
||||||
return sp & mangleName(v.symName)
|
return sp & mangleName(symName)
|
||||||
of ckList:
|
of ckList:
|
||||||
if v.items.len == 0:
|
if v.items.len == 0:
|
||||||
return sp & "cljList(@[])"
|
return sp & "cljList(@[])"
|
||||||
|
|||||||
Reference in New Issue
Block a user