From 7bf7929b7e61de7a06f0b6aefb14ae33803f8592 Mon Sep 17 00:00:00 2001 From: jmgomez Date: Thu, 28 Aug 2025 11:50:38 +0100 Subject: [PATCH 1/2] Adds support some `when` expressions in the declarative parser. --- src/nimblepkg/declarativeparser.nim | 185 +++++++++++++++++++++++++++- tests/tdeclarativeparser.nim | 182 ++++++++++++++++++++++++++- 2 files changed, 362 insertions(+), 5 deletions(-) diff --git a/src/nimblepkg/declarativeparser.nim b/src/nimblepkg/declarativeparser.nim index a2f8bbeb..36f8854b 100644 --- a/src/nimblepkg/declarativeparser.nim +++ b/src/nimblepkg/declarativeparser.nim @@ -29,6 +29,142 @@ type NimbleFileInfo* = object proc eqIdent(a, b: string): bool {.inline.} = cmpIgnoreCase(a, b) == 0 and a[0] == b[0] +proc compileDefines(): Table[string, bool] = + result = initTable[string, bool]() + result["windows"] = defined(windows) + result["posix"] = defined(posix) + result["linux"] = defined(linux) + result["android"] = defined(android) + result["macosx"] = defined(macosx) + result["freebsd"] = defined(freebsd) + result["openbsd"] = defined(openbsd) + result["netbsd"] = defined(netbsd) + result["solaris"] = defined(solaris) + result["amd64"] = defined(amd64) + result["x86_64"] = defined(x86_64) + result["i386"] = defined(i386) + result["arm"] = defined(arm) + result["arm64"] = defined(arm64) + result["mips"] = defined(mips) + result["powerpc"] = defined(powerpc) + # Common additional switches used in nimble files + result["js"] = defined(js) + result["emscripten"] = defined(emscripten) + result["wasm32"] = defined(wasm32) + result["mingw"] = defined(mingw) + +var definedSymbols: Table[string, bool] = compileDefines() + +proc getBasicDefines*(): Table[string, bool] = + return definedSymbols + +proc extractRequiresInfo*(nimbleFile: string, options: Options): NimbleFileInfo + +proc testEvaluateCondition*(nimbleFile: string): bool = + ## Test function to check if when defined() evaluation works + var options = initOptions() + let nimbleInfo = extractRequiresInfo(nimbleFile, options) + return not nimbleInfo.nestedRequires and not nimbleInfo.hasErrors + +proc setBasicDefines*(sym: string, value: bool) {.inline.} = + definedSymbols[sym] = value + +proc evalBasicDefines(sym: string; conf: ConfigRef; n: PNode): Option[bool] = + if sym in definedSymbols: + return some(definedSymbols[sym]) + else: + localError(conf, n.info, "undefined symbol: " & sym) + return none(bool) + +proc evalBooleanCondition(n: PNode; conf: ConfigRef): Option[bool] = + ## Recursively evaluate boolean conditions in when statements + case n.kind + of nkCall: + # Handle defined(platform) calls + if n[0].kind == nkIdent and n[0].ident.s == "defined" and n.len == 2: + if n[1].kind == nkIdent: + return evalBasicDefines(n[1].ident.s, conf, n) + return none(bool) + of nkInfix: + # Handle binary operators: and, or + if n[0].kind == nkIdent and n.len == 3: + case n[0].ident.s + of "and": + let left = evalBooleanCondition(n[1], conf) + let right = evalBooleanCondition(n[2], conf) + if left.isSome and right.isSome: + return some(left.get and right.get) + else: + return none(bool) + of "or": + let left = evalBooleanCondition(n[1], conf) + let right = evalBooleanCondition(n[2], conf) + if left.isSome and right.isSome: + return some(left.get or right.get) + else: + return none(bool) + of "xor": + let left = evalBooleanCondition(n[1], conf) + let right = evalBooleanCondition(n[2], conf) + if left.isSome and right.isSome: + return some(left.get xor right.get) + else: + return none(bool) + return none(bool) + of nkPrefix: + # Handle unary operators: not + if n[0].kind == nkIdent and n[0].ident.s == "not" and n.len == 2: + let inner = evalBooleanCondition(n[1], conf) + if inner.isSome: + return some(not inner.get) + else: + return none(bool) + return none(bool) + of nkPar: + # Handle parentheses - evaluate the content + if n.len == 1: + return evalBooleanCondition(n[0], conf) + return none(bool) + of nkIdent: + # Handle direct identifiers (though this shouldn't happen in practice) + return evalBasicDefines(n.ident.s, conf, n) + else: + return none(bool) + +proc isEvaluableCondition(n: PNode): bool = + ## Check if a when condition only contains evaluable expressions (defined() calls) + case n.kind + of nkCall: + # Allow defined(platform) calls + if n[0].kind == nkIdent and n[0].ident.s == "defined" and n.len == 2: + if n[1].kind == nkIdent: + return true + return false + of nkInfix: + # Allow binary operators: and, or, xor + if n[0].kind == nkIdent and n.len == 3: + case n[0].ident.s + of "and", "or", "xor": + return isEvaluableCondition(n[1]) and isEvaluableCondition(n[2]) + else: + return false + return false + of nkPrefix: + # Allow unary operators: not + if n[0].kind == nkIdent and n[0].ident.s == "not" and n.len == 2: + return isEvaluableCondition(n[1]) + return false + of nkPar: + # Allow parentheses - check the content + if n.len == 1: + return isEvaluableCondition(n[0]) + return false + of nkIdent: + # Allow direct identifiers if they're in our defined symbols + return n.ident.s in definedSymbols + else: + return false + proc collectRequiresFromNode(n: PNode, result: var seq[string]) = case n.kind of nkStmtList, nkStmtListExpr: @@ -53,7 +189,25 @@ proc validateNoNestedRequires(nfl: var NimbleFileInfo, n: PNode, conf: ConfigRef of nkStmtList, nkStmtListExpr: for child in n: validateNoNestedRequires(nfl, child, conf, hasErrors, nestedRequires, inControlFlow) - of nkWhenStmt, nkIfStmt, nkIfExpr, nkElifBranch, nkElse, nkElifExpr, nkElseExpr: + of nkWhenStmt: + # Special handling for when statements - allow evaluable conditions + var allowedWhen = false + if n.len > 0: + let firstBranch = n[0] + if firstBranch.kind == nkElifBranch and firstBranch.len >= 2: + let condition = firstBranch[0] + if isEvaluableCondition(condition): + allowedWhen = true + + if allowedWhen: + # This is an evaluable when statement - skip validation here, it will be handled in extract + # Don't validate children here - the extract phase will handle them properly + discard + else: + # This is a non-evaluable when statement - mark as control flow + for child in n: + validateNoNestedRequires(nfl, child, conf, hasErrors, nestedRequires, true) + of nkIfStmt, nkIfExpr, nkElifBranch, nkElse, nkElifExpr, nkElseExpr: for child in n: validateNoNestedRequires(nfl, child, conf, hasErrors, nestedRequires, true) of nkCallKinds: @@ -191,6 +345,35 @@ proc extract(n: PNode, conf: ConfigRef, result: var NimbleFileInfo, options: Opt result.bin[bin] = bin else: discard + of nkWhenStmt: + # Handle full when/elif/else chains. + var taken = false + var hasElse = false + + # Iterate all branches; choose the first with condition evaluating to true. + for i in 0 ..< n.len: + let br = n[i] + case br.kind + of nkElifBranch: + if br.len >= 2: + let cond = br[0] + let body = br[1] + let condResult = evalBooleanCondition(cond, conf) + if condResult.isSome: + if condResult.get and not taken: + extract(body, conf, result, options) + taken = true + else: + # Non-evaluable condition - skip this branch entirely + discard + of nkElse: + if br.len >= 1 and not taken: + let body = br[0] + extract(body, conf, result, options) + taken = true + hasElse = true + else: + discard else: discard diff --git a/tests/tdeclarativeparser.nim b/tests/tdeclarativeparser.nim index 1416a958..c87926da 100644 --- a/tests/tdeclarativeparser.nim +++ b/tests/tdeclarativeparser.nim @@ -38,12 +38,44 @@ suite "Declarative parsing": check pkg in requires.mapIt(it[0]) test "should detect nested requires and fail": - let nimbleFile = getNimbleFileFromPkgNameHelper("jester") - var options = initOptions() - let nimbleFileInfo = extractRequiresInfo(nimbleFile, options) + # Create a test file with actual nested requires (not when defined) + let testDir = "test_real_nested_requires" + createDir(testDir) + + writeFile(testDir / "nested.nimble", """ +version = "0.1.0" + +requires "nim >= 1.6.0" +when someRuntimeCondition: + requires "badlib" +""") + + var options = initOptions() + let nimbleFileInfo = extractRequiresInfo(testDir / "nested.nimble", options) + check nimbleFileInfo.nestedRequires - + + # Clean up + removeDir(testDir) + + test "should handle when defined() constructs and not mark as nested": + let nimbleFile = getNimbleFileFromPkgNameHelper("jester") + var options = initOptions() + let nimbleFileInfo = extractRequiresInfo(nimbleFile, options) + + # Should NOT be detected as nested requires since it uses when defined() + check not nimbleFileInfo.nestedRequires + check not nimbleFileInfo.hasErrors + + # Should have base requirement + check "nim >= 1.0.0" in nimbleFileInfo.requires + + # Should have platform-specific requirement based on current platform + when not defined(windows): + check "httpbeast >= 0.4.0" in nimbleFileInfo.requires + else: + check "httpbeast >= 0.4.0" notin nimbleFileInfo.requires test "should parse bin from a nimble file": let nimbleFile = getNimbleFileFromPkgNameHelper("nimlangserver") @@ -257,3 +289,145 @@ json_rpc # Clean up removeDir(testDir) + +suite "When defined() support": + test "should parse when defined() conditions for current platform": + let testDir = "test_when_defined" + createDir(testDir) + + # Create a nimble file with when defined() blocks + writeFile(testDir / "test.nimble", """ +version = "0.1.0" +author = "test" +description = "Test package for when defined support" +license = "MIT" + +requires "nim >= 1.6.0" + +when defined(windows): + requires "winapi" + +when defined(linux): + requires "linuxlib" + +when defined(macosx): + requires "macoslib" +""") + + var options = initOptions() + let nimbleFileInfo = extractRequiresInfo(testDir / "test.nimble", options) + + # Should not treat when defined() as nested requires + check not nimbleFileInfo.nestedRequires + check not nimbleFileInfo.hasErrors + + # Should have base requirement + check "nim >= 1.6.0" in nimbleFileInfo.requires + + # Should have platform-specific requirement based on current platform + when defined(windows): + check "winapi" in nimbleFileInfo.requires + check "linuxlib" notin nimbleFileInfo.requires + check "macoslib" notin nimbleFileInfo.requires + elif defined(linux): + check "linuxlib" in nimbleFileInfo.requires + check "winapi" notin nimbleFileInfo.requires + check "macoslib" notin nimbleFileInfo.requires + elif defined(macosx): + check "macoslib" in nimbleFileInfo.requires + check "winapi" notin nimbleFileInfo.requires + check "linuxlib" notin nimbleFileInfo.requires + + # Clean up + removeDir(testDir) + + test "should handle complex when defined() expressions": + let testDir = "test_when_complex" + createDir(testDir) + + writeFile(testDir / "test.nimble", """ +version = "0.1.0" + +requires "nim >= 1.6.0" + +when defined(windows) or defined(linux): + requires "posixcompat" + +when not defined(js): + requires "nativelib" + +when defined(windows) and defined(amd64): + requires "winx64lib" +""") + + var options = initOptions() + let nimbleFileInfo = extractRequiresInfo(testDir / "test.nimble", options) + + check not nimbleFileInfo.nestedRequires + check not nimbleFileInfo.hasErrors + check "nim >= 1.6.0" in nimbleFileInfo.requires + + # Check OR condition + when defined(windows) or defined(linux): + check "posixcompat" in nimbleFileInfo.requires + else: + check "posixcompat" notin nimbleFileInfo.requires + + # Check NOT condition + when not defined(js): + check "nativelib" in nimbleFileInfo.requires + else: + check "nativelib" notin nimbleFileInfo.requires + + # Check AND condition + when defined(windows) and defined(amd64): + check "winx64lib" in nimbleFileInfo.requires + else: + check "winx64lib" notin nimbleFileInfo.requires + + # Clean up + removeDir(testDir) + + test "should reject non-evaluable when conditions": + let testDir = "test_when_invalid" + createDir(testDir) + + writeFile(testDir / "test.nimble", """ +version = "0.1.0" + +requires "nim >= 1.6.0" + +when someRuntimeCondition: + requires "conditionallib" +""") + + var options = initOptions() + let nimbleFileInfo = extractRequiresInfo(testDir / "test.nimble", options) + + # Should treat non-evaluable when as nested requires + check nimbleFileInfo.nestedRequires + + # Clean up + removeDir(testDir) + + test "should reject non-evaluable when defined() conditions": + let testDir = "test_when_invalid" + createDir(testDir) + + writeFile(testDir / "test.nimble", """ +version = "0.1.0" + +requires "nim >= 1.6.0" + +when defined(whatever): + requires "conditionallib" +""") + + var options = initOptions() + let nimbleFileInfo = extractRequiresInfo(testDir / "test.nimble", options) + + # Should treat non-evaluable when as nested requires + check nimbleFileInfo.nestedRequires + + # Clean up + removeDir(testDir) \ No newline at end of file From 892a9a1321d1ea80b4b41702b8016a5ff1418f43 Mon Sep 17 00:00:00 2001 From: jmgomez Date: Fri, 29 Aug 2025 10:01:19 +0100 Subject: [PATCH 2/2] Uses `StringTableRef` to hold the defines --- src/nimblepkg/declarativeparser.nim | 54 ++++++++++++++--------------- tests/tdeclarativeparser.nim | 22 +----------- 2 files changed, 28 insertions(+), 48 deletions(-) diff --git a/src/nimblepkg/declarativeparser.nim b/src/nimblepkg/declarativeparser.nim index 36f8854b..4714f1dc 100644 --- a/src/nimblepkg/declarativeparser.nim +++ b/src/nimblepkg/declarativeparser.nim @@ -10,7 +10,7 @@ from compiler/nimblecmd import getPathVersionChecksum import version, packageinfotypes, packageinfo, options, packageparser, cli, packagemetadatafile, common import sha1hashes, vcstools, urls -import std/[tables, sequtils, strscans, strformat, os, options] +import std/[tables, sequtils, strscans, strformat, os, options, strtabs] type NimbleFileInfo* = object nimbleFile*: string @@ -29,33 +29,33 @@ type NimbleFileInfo* = object proc eqIdent(a, b: string): bool {.inline.} = cmpIgnoreCase(a, b) == 0 and a[0] == b[0] -proc compileDefines(): Table[string, bool] = - result = initTable[string, bool]() - result["windows"] = defined(windows) - result["posix"] = defined(posix) - result["linux"] = defined(linux) - result["android"] = defined(android) - result["macosx"] = defined(macosx) - result["freebsd"] = defined(freebsd) - result["openbsd"] = defined(openbsd) - result["netbsd"] = defined(netbsd) - result["solaris"] = defined(solaris) - result["amd64"] = defined(amd64) - result["x86_64"] = defined(x86_64) - result["i386"] = defined(i386) - result["arm"] = defined(arm) - result["arm64"] = defined(arm64) - result["mips"] = defined(mips) - result["powerpc"] = defined(powerpc) +proc compileDefines(): StringTableRef = + result = newStringTable(modeCaseSensitive) + result["windows"] = $defined(windows) + result["posix"] = $defined(posix) + result["linux"] = $defined(linux) + result["android"] = $defined(android) + result["macosx"] = $defined(macosx) + result["freebsd"] = $defined(freebsd) + result["openbsd"] = $defined(openbsd) + result["netbsd"] = $defined(netbsd) + result["solaris"] = $defined(solaris) + result["amd64"] = $defined(amd64) + result["x86_64"] = $defined(x86_64) + result["i386"] = $defined(i386) + result["arm"] = $defined(arm) + result["arm64"] = $defined(arm64) + result["mips"] = $defined(mips) + result["powerpc"] = $defined(powerpc) # Common additional switches used in nimble files - result["js"] = defined(js) - result["emscripten"] = defined(emscripten) - result["wasm32"] = defined(wasm32) - result["mingw"] = defined(mingw) + result["js"] = $defined(js) + result["emscripten"] = $defined(emscripten) + result["wasm32"] = $defined(wasm32) + result["mingw"] = $defined(mingw) -var definedSymbols: Table[string, bool] = compileDefines() +var definedSymbols: StringTableRef = compileDefines() -proc getBasicDefines*(): Table[string, bool] = +proc getBasicDefines*(): StringTableRef = return definedSymbols proc extractRequiresInfo*(nimbleFile: string, options: Options): NimbleFileInfo @@ -67,11 +67,11 @@ proc testEvaluateCondition*(nimbleFile: string): bool = return not nimbleInfo.nestedRequires and not nimbleInfo.hasErrors proc setBasicDefines*(sym: string, value: bool) {.inline.} = - definedSymbols[sym] = value + definedSymbols[sym] = $value proc evalBasicDefines(sym: string; conf: ConfigRef; n: PNode): Option[bool] = if sym in definedSymbols: - return some(definedSymbols[sym]) + return some(definedSymbols[sym] == "true") else: localError(conf, n.info, "undefined symbol: " & sym) return none(bool) diff --git a/tests/tdeclarativeparser.nim b/tests/tdeclarativeparser.nim index c87926da..1b40a07f 100644 --- a/tests/tdeclarativeparser.nim +++ b/tests/tdeclarativeparser.nim @@ -410,24 +410,4 @@ when someRuntimeCondition: # Clean up removeDir(testDir) - test "should reject non-evaluable when defined() conditions": - let testDir = "test_when_invalid" - createDir(testDir) - - writeFile(testDir / "test.nimble", """ -version = "0.1.0" - -requires "nim >= 1.6.0" - -when defined(whatever): - requires "conditionallib" -""") - - var options = initOptions() - let nimbleFileInfo = extractRequiresInfo(testDir / "test.nimble", options) - - # Should treat non-evaluable when as nested requires - check nimbleFileInfo.nestedRequires - - # Clean up - removeDir(testDir) \ No newline at end of file + \ No newline at end of file