nandi/clonimpublic Fork
⑂ 017649f
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/clonim.git
git clone ssh://git@rickub.com/nandi/clonim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

perf: cache the Nim build so repeat runs skip the backend

`clonim run` pointed --nimcache at a per-PID temp dir that was deleted on
exit, so every run recompiled runtime.nim, core.nim and the Nim stdlib from
scratch. Analysis took 1.8ms; the backend took 5.2s.

The nimcache is now persistent, keyed by the source file's absolute path and
the build flags. On top of that, `run` skips the backend entirely when a stamp
file shows that nothing feeding the binary has changed: the generated Nim, the
sources in src/, the flags, and the Nim compiler itself. Invalidation is
content-based, so touching a file without editing it doesn't force a rebuild.

Repeat runs of examples/intrinsics.clj go from 5.33s to 0.005s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-20T18:13:32-07:00 Browse files
017649f parent: 430bb9f
modified src/clonim.nim +36 -6
@@ -3,7 +3,7 @@
33 ## clonim run foo.clj compile and run
44 ## clonim build foo.clj [-o bin] compile to a native binary
55 ## clonim emit foo.clj print the generated Nim
6-import std/[os, osproc, strutils, times]
6+import std/[hashes, os, osproc, strutils, times]
77 import runtime, compiler
88
99 proc usage() =
@@ -27,6 +27,17 @@ proc srcDir(): string =
2727 if fileExists(cand / "runtime.nim"): return cand
2828 getAppDir()
2929
30+proc buildKey(nimSrc: string, release: bool): string =
31+ ## Identifies everything the produced binary depends on. Any change here
32+ ## invalidates the cached binary for a source file.
33+ var h: Hash = hash(nimSrc) !& hash(release)
34+ for f in walkFiles(srcDir() / "*.nim"):
35+ h = h !& hash(readFile(f))
36+ let nimExe = findExe("nim")
37+ if nimExe.len > 0:
38+ h = h !& hash($getLastModificationTime(nimExe))
39+ $(!$h)
40+
3041 proc main() =
3142 let argv = commandLineParams()
3243 if argv.len < 2: usage()
@@ -71,9 +82,15 @@ proc main() =
7182 for ch in stem:
7283 modName.add (if ch in {'a'..'z', 'A'..'Z', '0'..'9'}: ch else: '_')
7384 if modName.len == 0 or modName[0] in {'0'..'9'}: modName = "m" & modName
74- let work = getTempDir() / ("clonim-" & modName & "-" & $getCurrentProcessId())
85+ # The nimcache is persistent and keyed by the source file's absolute path, so
86+ # repeated `clonim run` on the same file reuses the compiled runtime/core and
87+ # the Nim stdlib instead of rebuilding them from scratch every time. The .nim
88+ # file lives in the same directory so its path stays stable across runs too —
89+ # Nim keys its cache entries on module paths.
90+ let work = getTempDir() / "clonim" /
91+ (modName & "-" & toHex(hash(file.absolutePath).uint32, 8) &
92+ (if release: "-r" else: ""))
7593 createDir(work)
76- defer: removeDir(work)
7794 let nimFile = work / (modName & ".nim")
7895 writeFile(nimFile, nimSrc)
7996
@@ -81,15 +98,27 @@ proc main() =
8198 outBin = (if cmd == "build": stem else: work / modName)
8299 outBin = outBin.absolutePath
83100
101+ # Even a warm nimcache costs a second or so of semantic checking and linking.
102+ # `run` skips the backend entirely when nothing that feeds the binary has
103+ # changed: the generated Nim, the runtime/core sources it imports, the build
104+ # flags, and the Nim compiler itself.
105+ let stamp = work / "stamp"
106+ let cached = cmd == "run" and fileExists(outBin) and fileExists(stamp) and
107+ readFile(stamp) == buildKey(nimSrc, release)
108+
84109 var nimCmd = @["nim", "c", "--hints:off", "--warnings:off",
85110 "--path:" & srcDir(), "--nimcache:" & (work / "cache"),
86111 "-o:" & outBin]
87112 if release: nimCmd.add "-d:release"
88113 nimCmd.add nimFile
89- if verbose: echo "clonim: " & nimCmd.join(" ")
114+ if verbose and not cached: echo "clonim: " & nimCmd.join(" ")
90115
91116 let t1 = epochTime()
92- let (output, code) = execCmdEx(nimCmd.join(" "))
117+ var output = ""
118+ var code = 0
119+ if not cached:
120+ (output, code) = execCmdEx(nimCmd.join(" "))
121+ if code == 0: writeFile(stamp, buildKey(nimSrc, release))
93122 let tBuild = epochTime() - t1
94123 if code != 0:
95124 stderr.writeLine("clonim: Nim backend failed\n" & output)
@@ -97,7 +126,8 @@ proc main() =
97126 quit(1)
98127 if verbose:
99128 echo "clonim: analyze ", (tCompile * 1000).formatFloat(ffDecimal, 1), "ms ",
100- "nim ", (tBuild * 1000).formatFloat(ffDecimal, 1), "ms"
129+ "nim ", (tBuild * 1000).formatFloat(ffDecimal, 1), "ms",
130+ (if cached: " (cached)" else: "")
101131
102132 case cmd
103133 of "build":
@@ -3,7 +3,7 @@
3 ## clonim run foo.clj compile and run3 ## clonim run foo.clj compile and run
4 ## clonim build foo.clj [-o bin] compile to a native binary4 ## clonim build foo.clj [-o bin] compile to a native binary
5 ## clonim emit foo.clj print the generated Nim5 ## clonim emit foo.clj print the generated Nim
6-import std/[os, osproc, strutils, times]6+import std/[hashes, os, osproc, strutils, times]
7 import runtime, compiler7 import runtime, compiler
8 8
9 proc usage() =9 proc usage() =
@@ -27,6 +27,17 @@ proc srcDir(): string =
27 if fileExists(cand / "runtime.nim"): return cand27 if fileExists(cand / "runtime.nim"): return cand
28 getAppDir()28 getAppDir()
29 29
30+proc buildKey(nimSrc: string, release: bool): string =
31+ ## Identifies everything the produced binary depends on. Any change here
32+ ## invalidates the cached binary for a source file.
33+ var h: Hash = hash(nimSrc) !& hash(release)
34+ for f in walkFiles(srcDir() / "*.nim"):
35+ h = h !& hash(readFile(f))
36+ let nimExe = findExe("nim")
37+ if nimExe.len > 0:
38+ h = h !& hash($getLastModificationTime(nimExe))
39+ $(!$h)
40+
30 proc main() =41 proc main() =
31 let argv = commandLineParams()42 let argv = commandLineParams()
32 if argv.len < 2: usage()43 if argv.len < 2: usage()
@@ -71,9 +82,15 @@ proc main() =
71 for ch in stem:82 for ch in stem:
72 modName.add (if ch in {'a'..'z', 'A'..'Z', '0'..'9'}: ch else: '_')83 modName.add (if ch in {'a'..'z', 'A'..'Z', '0'..'9'}: ch else: '_')
73 if modName.len == 0 or modName[0] in {'0'..'9'}: modName = "m" & modName84 if modName.len == 0 or modName[0] in {'0'..'9'}: modName = "m" & modName
74- let work = getTempDir() / ("clonim-" & modName & "-" & $getCurrentProcessId())85+ # The nimcache is persistent and keyed by the source file's absolute path, so
86+ # repeated `clonim run` on the same file reuses the compiled runtime/core and
87+ # the Nim stdlib instead of rebuilding them from scratch every time. The .nim
88+ # file lives in the same directory so its path stays stable across runs too —
89+ # Nim keys its cache entries on module paths.
90+ let work = getTempDir() / "clonim" /
91+ (modName & "-" & toHex(hash(file.absolutePath).uint32, 8) &
92+ (if release: "-r" else: ""))
75 createDir(work)93 createDir(work)
76- defer: removeDir(work)
77 let nimFile = work / (modName & ".nim")94 let nimFile = work / (modName & ".nim")
78 writeFile(nimFile, nimSrc)95 writeFile(nimFile, nimSrc)
79 96
@@ -81,15 +98,27 @@ proc main() =
81 outBin = (if cmd == "build": stem else: work / modName)98 outBin = (if cmd == "build": stem else: work / modName)
82 outBin = outBin.absolutePath99 outBin = outBin.absolutePath
83 100
101+ # Even a warm nimcache costs a second or so of semantic checking and linking.
102+ # `run` skips the backend entirely when nothing that feeds the binary has
103+ # changed: the generated Nim, the runtime/core sources it imports, the build
104+ # flags, and the Nim compiler itself.
105+ let stamp = work / "stamp"
106+ let cached = cmd == "run" and fileExists(outBin) and fileExists(stamp) and
107+ readFile(stamp) == buildKey(nimSrc, release)
108+
84 var nimCmd = @["nim", "c", "--hints:off", "--warnings:off",109 var nimCmd = @["nim", "c", "--hints:off", "--warnings:off",
85 "--path:" & srcDir(), "--nimcache:" & (work / "cache"),110 "--path:" & srcDir(), "--nimcache:" & (work / "cache"),
86 "-o:" & outBin]111 "-o:" & outBin]
87 if release: nimCmd.add "-d:release"112 if release: nimCmd.add "-d:release"
88 nimCmd.add nimFile113 nimCmd.add nimFile
89- if verbose: echo "clonim: " & nimCmd.join(" ")114+ if verbose and not cached: echo "clonim: " & nimCmd.join(" ")
90 115
91 let t1 = epochTime()116 let t1 = epochTime()
92- let (output, code) = execCmdEx(nimCmd.join(" "))117+ var output = ""
118+ var code = 0
119+ if not cached:
120+ (output, code) = execCmdEx(nimCmd.join(" "))
121+ if code == 0: writeFile(stamp, buildKey(nimSrc, release))
93 let tBuild = epochTime() - t1122 let tBuild = epochTime() - t1
94 if code != 0:123 if code != 0:
95 stderr.writeLine("clonim: Nim backend failed\n" & output)124 stderr.writeLine("clonim: Nim backend failed\n" & output)
@@ -97,7 +126,8 @@ proc main() =
97 quit(1)126 quit(1)
98 if verbose:127 if verbose:
99 echo "clonim: analyze ", (tCompile * 1000).formatFloat(ffDecimal, 1), "ms ",128 echo "clonim: analyze ", (tCompile * 1000).formatFloat(ffDecimal, 1), "ms ",
100- "nim ", (tBuild * 1000).formatFloat(ffDecimal, 1), "ms"129+ "nim ", (tBuild * 1000).formatFloat(ffDecimal, 1), "ms",
130+ (if cached: " (cached)" else: "")
101 131
102 case cmd132 case cmd
103 of "build":133 of "build":