Is there some way to print all defines used at compile time?
# test.nim
printAllDefines()
# config.nims
--define : also-grab-this-define
nim r -d:arbitrary --define:ssl test.nimThis works - thanks Claude Code!
nim r -d:test test.nim
import definedump
const mine = userDefines() # seq[string], nim* symbols filtered out, usable at runtime
const all = allDefines() # everything including implementation symbols
#printAllDefines() # echo all of them at compile time
echo "── user-defined compile-time defines ──"
for d in mine: echo " ", d
echo "── all compile-time defines (including nim* implementation symbols) ──"
for d in all: echo " ", d
## definedump.nim — enumerate every compile-time define from inside the program.
##
## Portable(ish) strategy:
## 1. Recover the *running compiler's* command line so we can see the
## -d:/--define: flags this build was invoked with. A macro that shells out
## to `nim dump` spawns a FRESH compiler that otherwise can't see them.
## • Linux : /proc/$PPID/cmdline (the shell staticExec spawns has nim as parent)
## • macOS : ps on $PPID
## • Windows: query the nim.exe process via PowerShell CIM
## 2. Re-run `nim dump --dump.format:json`, forwarding those define flags, and
## let nim merge command line + config files + implicit symbols for us.
##
## Compile with -d:definedumpDebug to echo the raw recovered cmdline + dump output.
import std/[macros, json, strutils, compilesettings]
proc recoverCompilerCmdline(): string {.compileTime.} =
when defined(windows):
# Returns the CommandLine of every nim.exe currently running (newline-sep).
# During the build that's our compiler. Single-quote doubling avoids backslash hell.
const winCmd = """powershell -NoProfile -Command "(Get-CimInstance Win32_Process -Filter 'Name=''nim.exe''').CommandLine" """
result = staticExec(winCmd)
elif defined(macosx):
result = staticExec("ps -ww -o args= -p $PPID")
else:
result = staticExec("tr '\\0' ' ' < /proc/$PPID/cmdline")
proc extractDefineFlags(cmdline: string): string {.compileTime.} =
var flags: seq[string]
for raw in cmdline.splitWhitespace():
let tok = raw.strip()
if tok.startsWith("-d:") or tok.startsWith("--define:") or
tok.startsWith("-u:") or tok.startsWith("--undef:"):
if tok notin flags: flags.add tok
result = flags.join(" ")
proc sliceJson(s: string): string {.compileTime.} =
## tolerate BOMs / hint lines / progress dots before the JSON object
let a = s.find('{')
let b = s.rfind('}')
if a >= 0 and b >= a: s[a .. b] else: ""
proc dumpSymbols(): seq[string] {.compileTime.} =
let parentCmd = recoverCompilerCmdline()
let defineFlags = extractDefineFlags(parentCmd)
let proj = querySetting(projectFull)
let cmd = "nim dump --dump.format:json --hints:off " & defineFlags &
" \"" & proj & "\""
let rawOut = staticExec(cmd)
let jsonStr = sliceJson(rawOut)
when defined(definedumpDebug):
echo "── definedump debug ──"
echo "recovered cmdline : ", parentCmd
echo "forwarded defines : ", defineFlags
echo "dump command : ", cmd
echo "raw dump output :\n", rawOut
echo "──────────────────────"
if jsonStr.len == 0:
error("definedump: `nim dump` produced no JSON. Re-run with " &
"-d:definedumpDebug to see its raw output.\nRaw was:\n" & rawOut)
for s in parseJson(jsonStr)["defined_symbols"]:
result.add s.getStr
macro allDefines*(): untyped =
## seq[string] of ALL defined symbols (including nim* implementation symbols).
result = newNimNode(nnkBracket)
for s in dumpSymbols(): result.add newLit(s)
result = nnkPrefix.newTree(ident"@", result)
macro userDefines*(): untyped =
## seq[string] of defines minus the nim*-reserved implementation symbols.
result = newNimNode(nnkBracket)
for s in dumpSymbols():
if not s.startsWith("nim"): result.add newLit(s)
result = nnkPrefix.newTree(ident"@", result)
template printAllDefines*() =
static:
echo "── all compile-time defines ──"
for d in allDefines(): echo " ", d
That code treats whitespace as if spaces will never occur in paths (doubled up even, i.e. (for...splitWhitespace: tok = .strip() ?). Consider a source file named "my dir / file -d:blah foo.nim" which, while unlikely, is possible. It also neglects both --define=sym and -u=sym and their counterparts. Also, nim dump --verbosity=0 might eliminate the need for sliceJson.
Really, though, you could bypass all of that CL-parsing past the "subcommand root" and the OS portability stuff and probably still work on BSDs and such by just starting from querySetting(commandLine) directly (e.g., check for startsWith and then replace " " & command.querySetting & " " with "nim dump --dump.format:json --hints=off --verbosity=0 " - or maybe a start-of-string anchored regex subst with any amount of white space).
Re-invoking the compiler is kind of expensive. It's also hard to know how well querySetting(commandLine) quotes its args for all shell evaluators. So, a more efficient & robust answer is probably to just add a MultipleValueSetting { say, definedSymbols or definedSymbolKeys } in std/compilesettings that pulls keys out of conf.symbols in the appropriate spot of compiler/vmops.nim. Also, starting with "nim" is just a convention.
However, to get a PR like that through on github, you are going to have to motivate why "point queries" like when defined sym: are not enough for your actual use case. E.g., I know C compilers often provide echo|cc -dM -E - (even though C standards never cover any of that stuff since at least MSFT used very different CL syntax). I could imagine build-systems building off of that capability (e.g. forming a hash of all of them for some reason), but you didn't say why you needed this info. I'm sure I'm not the only one curious. And there may be a better way to achieve your actual goals.
Also, the name space is not guarded. So, nothing stops a user from saying -d:nimMyFoo or in their nim.cfg's or config.nims or etc., but you didn't say why you wanted that either. I can how the separation may have seemed to fall out of your implementation, but I define things in nim.cfg all the time (like useCligen or useAdix for optional willingness to depend), not just on compiler CLs (and CL vs. configs may be a more interesting separation, but for that you are back to parsing the CL).
Oh, I forgot to mention another gotcha not covered by that code, namely --unDef=. This also applies to a complicating aspect of my weak suggestion (the strong one is adding a VM op) which is that command.querySetting comes in normalized while commandLine.querySetting is raw. So, if you want this to work with nim c_Pp ... then you have to compare the prefix style insensitively, such as like this:
import std/[compilesettings, json, strutils, macros]
iterator normalized*(s: string): (int, int, char) =
var j = 0 ## Iterate over normalized bytes of string `s`.
for i in 0 ..< s.len: #XXX refactor strutils.normalize with this??
if s[i] in UppercaseLetters:
yield (i, j, chr(ord(s[i]) + (ord('a') - ord('A')))); inc j
elif s[i] != '_':
yield (i, j, s[i]); inc j
proc dumpSymbols*: seq[string] {.compileTime.} =
##[ Nim VM `querySettingSeq` has no `definedSymbols` (yet) & its `commandArgs`
only contains paths to .nim files (i.e. args not options). ]##
let cl = commandLine.querySetting.strip()
let c = command.querySetting.normalize # already normalized
var clSkip = -1
for (i, j, ch) in cl.normalized:
if j == c.len: clSkip = i; break # prefix matched
if j > c.len or ch != c[j]: # prefix mismatch
error "cl=\"" & cl & "\" does not normStartWith c=\"" & c & "\"\n"
let se = "nim dump --dump.format:json --verbosity=0" & cl[clSkip..^1].
multiReplace((" -r ", " "), (" --run ", " "), #XXX std/pegs subst
(" --run:on ", " "), (" --run=on ", " "))
try: #XXX Other flags can break; cl.parseCmdLine, take ONLY define/undef's
for s in se.staticExec.parseJson["defined_symbols"]:
result.add s.getStr
except CatchableError as e:
error "Bad/No JSON(" & e.msg & ") from: " & se & "\n"
when isMainModule: # Compiler/condsyms=>startsWith"nim" BUT user can -d=nimX
import std/algorithm; echo dumpSymbols().sorted.join "\n"
but also exhibited by this code is the problem with not doing a real full command-line re-parse (in this different add a prefix model): all that multiReplace fix-up should really be some kind of std/pegs style insensitive substitution (which I suppose could be extended to the first argument, but you also still need to be careful to not screw up right-hand-sides of ':' or '=' stuff throughout anyway).
To me this all really just argues for the strong suggestion to add the VM op. ;-) IF you have a good use case.
Another thing I neglected to mention is that if the use case is some kind of "settings hash" (just a guess, really) then the values (right-hand sides) probably do matter for correctness, not just the keys, but you haven't really said why you need this. So, it's not clear what kind of vm op would be best. E.g., a seq of (key,value) pairs might be better and it looks like the nim dump json does not presently emit anything other than true/false bool flag values.