summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--changelog.md2
-rw-r--r--compiler/docgen.nim88
-rw-r--r--lib/system.nim11
-rw-r--r--nimdoc/tester.nim5
-rw-r--r--nimdoc/testproject/expected/testproject.html2
-rw-r--r--tests/nimdoc/trunnableexamples.nim35
6 files changed, 105 insertions, 38 deletions
diff --git a/changelog.md b/changelog.md
index 88619b1c2..0e1af73c1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -185,6 +185,8 @@ proc mydiv(a, b): int {.raises: [].} =
   use `--doccmd:skip` to skip runnableExamples and rst test snippets.
 - new flag `--usenimcache` to output to nimcache (whatever it resolves to after all commands are processed)
   and avoids polluting both $pwd and $projectdir. It can be used with any command.
+- `runnableExamples "-b:cpp -r:off": code` is now supported, allowing to override how an example is compiled and run,
+  for example to change backend or compile only.
 
 ## Tool changes
 
diff --git a/compiler/docgen.nim b/compiler/docgen.nim
index 6123d49cf..8b5201029 100644
--- a/compiler/docgen.nim
+++ b/compiler/docgen.nim
@@ -17,7 +17,7 @@ import
   packages/docutils/rst, packages/docutils/rstgen,
   json, xmltree, cgi, trees, types,
   typesrenderer, astalgo, lineinfos, intsets,
-  pathutils, trees
+  pathutils, trees, tables
 
 const
   exportSection = skField
@@ -26,6 +26,12 @@ const
 
 type
   TSections = array[TSymKind, Rope]
+  ExampleGroup = ref object
+    ## a group of runnableExamples with same rdoccmd
+    rdoccmd: string ## from 1st arg in `runnableExamples(rdoccmd): body`
+    docCmd: string ## from user config, eg --doccmd:-d:foo
+    code: string ## contains imports; each import contains `body`
+    index: int ## group index
   TDocumentor = object of rstgen.RstGenerator
     modDesc: Rope       # module description
     module: PSym
@@ -44,7 +50,7 @@ type
                     # already. See bug #3655
     destFile*: AbsoluteFile
     thisDir*: AbsoluteDir
-    examples: string
+    exampleGroups: OrderedTable[string, ExampleGroup]
     wroteCss*: bool
 
   PDoc* = ref TDocumentor ## Alias to type less.
@@ -443,54 +449,72 @@ proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var Rope; renderFlags: TRe
       dispA(d.conf, result, "<span class=\"Other\">$1</span>", "\\spanOther{$1}",
             [escLit])
 
-proc testExample(d: PDoc; ex: PNode) =
+proc exampleOutputDir(d: PDoc): AbsoluteDir = d.conf.getNimcacheDir / RelativeDir"runnableExamples"
+
+proc writeExample(d: PDoc; ex: PNode, rdoccmd: string) =
   if d.conf.errorCounter > 0: return
-  let outputDir = d.conf.getNimcacheDir / RelativeDir"runnableExamples"
+  let outputDir = d.exampleOutputDir
   createDir(outputDir)
   inc d.exampleCounter
+  # PRTEMP
   let outp = outputDir / RelativeFile(extractFilename(d.filename.changeFileExt"" &
       "_examples" & $d.exampleCounter & ".nim"))
   #let nimcache = outp.changeFileExt"" & "_nimcache"
   renderModule(ex, d.filename, outp.string, conf = d.conf)
-  d.examples.add "import r\"" & outp.string & "\"\n"
+  if rdoccmd notin d.exampleGroups: d.exampleGroups[rdoccmd] = ExampleGroup(rdoccmd: rdoccmd, docCmd: d.conf.docCmd, index: d.exampleGroups.len)
+  d.exampleGroups[rdoccmd].code.add "import r\"$1\"\n" % outp.string
 
 proc runAllExamples(d: PDoc) =
-  let docCmd = d.conf.docCmd
   let backend = d.conf.backend
   # This used to be: `let backend = if isDefined(d.conf, "js"): "js"` (etc), however
   # using `-d:js` (etc) cannot work properly, eg would fail with `importjs`
   # since semantics are affected by `config.backend`, not by isDefined(d.conf, "js")
-  if d.examples.len == 0 or docCmd == docCmdSkip: return
-  let outputDir = d.conf.getNimcacheDir / RelativeDir"runnableExamples"
-  let outp = outputDir / RelativeFile(extractFilename(d.filename.changeFileExt"" &
-      "_examples.nim"))
-  writeFile(outp, d.examples)
-  let cmd = "$nim $backend -r --warning:UnusedImport:off --path:$path --nimcache:$nimcache $docCmd $file" % [
-    "nim", os.getAppFilename(),
-    "backend", $d.conf.backend,
-    "path", quoteShell(d.conf.projectPath),
-    "nimcache", quoteShell(outputDir),
-    "file", quoteShell(outp),
-    "docCmd", docCmd,
-  ]
-  if os.execShellCmd(cmd) != 0:
-    quit "[runnableExamples] failed: generated file: '$1' cmd: $2" % [outp.string, cmd]
-  else:
-    # keep generated source file `outp` to allow inspection.
-    rawMessage(d.conf, hintSuccess, ["runnableExamples: " & outp.string])
-    removeFile(outp.changeFileExt(ExeExt))
-
+  let outputDir = d.exampleOutputDir
+  for _, group in d.exampleGroups:
+    if group.docCmd == docCmdSkip: continue
+    let outp = outputDir / RelativeFile("$1_group$2_examples.nim" % [d.filename.splitFile.name, $group.index])
+    group.code = "# autogenerated by docgen\n# source: $1\n# rdoccmd: $2\n$3" % [d.filename, group.rdoccmd, group.code]
+    writeFile(outp, group.code)
+    # most useful semantics is that `docCmd` comes after `rdoccmd`, so that we can (temporarily) override
+    # via command line
+    let cmd = "$nim $backend -r --warning:UnusedImport:off --path:$path --nimcache:$nimcache $rdoccmd $docCmd $file" % [
+      "nim", os.getAppFilename(),
+      "backend", $d.conf.backend,
+      "path", quoteShell(d.conf.projectPath),
+      "nimcache", quoteShell(outputDir),
+      "file", quoteShell(outp),
+      "rdoccmd", group.rdoccmd,
+      "docCmd", group.docCmd,
+    ]
+    if os.execShellCmd(cmd) != 0:
+      quit "[runnableExamples] failed: generated file: '$1' group: '$2' cmd: $3" % [outp.string, $group[], cmd]
+    else:
+      # keep generated source file `outp` to allow inspection.
+      rawMessage(d.conf, hintSuccess, ["runnableExamples: " & outp.string])
+      # removeFile(outp.changeFileExt(ExeExt)) # it's in nimcache, no need to remove
+
+proc prepareExample(d: PDoc; n: PNode): string =
+  ## returns `rdoccmd` for this runnableExamples
+  var rdoccmd = ""
+  if n.len < 2 or n.len > 3: globalError(d.conf, n.info, "runnableExamples invalid")
+  if n.len == 3:
+    let n1 = n[1]
+    # xxx this should be evaluated during sempass
+    if n1.kind notin nkStrKinds: globalError(d.conf, n1.info, "string litteral expected")
+    rdoccmd = n1.strVal
 
-proc prepareExamples(d: PDoc; n: PNode) =
   var docComment = newTree(nkCommentStmt)
   let loc = d.conf.toFileLineCol(n.info)
-  docComment.comment = "autogenerated by docgen from " & loc
+
+  docComment.comment = "autogenerated by docgen\nloc: $1\nrdoccmd: $2" % [loc, rdoccmd]
   var runnableExamples = newTree(nkStmtList,
       docComment,
       newTree(nkImportStmt, newStrNode(nkStrLit, d.filename)))
   runnableExamples.info = n.info
+
   for a in n.lastSon: runnableExamples.add a
-  testExample(d, runnableExamples)
+  writeExample(d, runnableExamples, rdoccmd)
+  result = rdoccmd
   when false:
     proc extractImports(n: PNode; result: PNode) =
       if n.kind in {nkImportStmt, nkImportExceptStmt, nkFromStmt}:
@@ -510,9 +534,11 @@ proc getAllRunnableExamplesRec(d: PDoc; n, orig: PNode; dest: var Rope) =
   of nkCallKinds:
     if isRunnableExamples(n[0]) and
         n.len >= 2 and n.lastSon.kind == nkStmtList:
-      prepareExamples(d, n)
+      let rdoccmd = prepareExample(d, n)
+      var msg = "Example:"
+      if rdoccmd.len > 0: msg.add " cmd: " & rdoccmd
       dispA(d.conf, dest, "\n<p><strong class=\"examples_text\">$1</strong></p>\n",
-          "\n\\textbf{$1}\n", [rope"Examples:"])
+          "\n\\textbf{$1}\n", [msg.rope])
       inc d.listingCounter
       let id = $d.listingCounter
       dest.add(d.config.getOrDefault"doc.listing_start" % [id, "langNim"])
diff --git a/lib/system.nim b/lib/system.nim
index e5b359c70..85be43cba 100644
--- a/lib/system.nim
+++ b/lib/system.nim
@@ -117,7 +117,7 @@ else:
     Ordinal* = OrdinalImpl | uint | uint64
 
 when defined(nimHasRunnableExamples):
-  proc runnableExamples*(body: untyped) {.magic: "RunnableExamples".}
+  proc runnableExamples*(rdoccmd = "", body: untyped) {.magic: "RunnableExamples".}
     ## A section you should use to mark `runnable example`:idx: code with.
     ##
     ## - In normal debug and release builds code within
@@ -139,10 +139,15 @@ when defined(nimHasRunnableExamples):
     ##       assert double(5) == 10
     ##       block: ## at block scope
     ##         defer: echo "done"
-    ##
     ##     result = 2 * x
+    ##     runnableExamples "-d:foo -b:cpp":
+    ##       import std/compilesettings
+    ##       doAssert querySetting(backend) == "cpp"
+    ##     runnableExamples "-r:off": ## this one is only compiled
+    ##        import std/browsers
+    ##        openDefaultBrowser "https://forum.nim-lang.org/"
 else:
-  template runnableExamples*(body: untyped) =
+  template runnableExamples*(doccmd = "", body: untyped) =
     discard
 
 proc declared*(x: untyped): bool {.magic: "Defined", noSideEffect, compileTime.}
diff --git a/nimdoc/tester.nim b/nimdoc/tester.nim
index a1500455e..58272a9b6 100644
--- a/nimdoc/tester.nim
+++ b/nimdoc/tester.nim
@@ -24,12 +24,13 @@ proc testNimDoc(prjDir, docsDir: string; switches: NimSwitches; fixup = false) =
     nimBuildIndexSwitches = switches.buildIndex.join(" ")
 
   putEnv("SOURCE_DATE_EPOCH", "100000")
+  const nimExe = getCurrentCompilerExe() # so that `bin/nim_temp r nimdoc/tester.nim` works
 
   if nimDocSwitches != "":
-    exec("nim doc $1" % [nimDocSwitches])
+    exec("$1 doc $2" % [nimExe, nimDocSwitches])
 
   if nimBuildIndexSwitches != "":
-    exec("nim buildIndex $1" % [nimBuildIndexSwitches])
+    exec("$1 buildIndex $2" % [nimExe, nimBuildIndexSwitches])
 
   for expected in walkDirRec(prjDir / "expected/"):
     let produced = expected.replace('\\', '/').replace("/expected/", "/$1/" % [docsDir])
diff --git a/nimdoc/testproject/expected/testproject.html b/nimdoc/testproject/expected/testproject.html
index 77f178265..4eb8ed44c 100644
--- a/nimdoc/testproject/expected/testproject.html
+++ b/nimdoc/testproject/expected/testproject.html
@@ -183,7 +183,7 @@ function main() {
   <div id="tocRoot"></div>
   
   <p class="module-desc">This is the top level module.
-<p><strong class="examples_text">Examples:</strong></p>
+<p><strong class="examples_text">Example:</strong></p>
 <pre class="listing"><span class="Keyword">import</span>
   <span class="Identifier">subdir</span> <span class="Operator">/</span> <span class="Identifier">subdir_b</span> <span class="Operator">/</span> <span class="Identifier">utils</span>
 
diff --git a/tests/nimdoc/trunnableexamples.nim b/tests/nimdoc/trunnableexamples.nim
index e3ae6b9cb..73cfacd43 100644
--- a/tests/nimdoc/trunnableexamples.nim
+++ b/tests/nimdoc/trunnableexamples.nim
@@ -1,5 +1,5 @@
 discard """
-cmd: "nim doc $file"
+cmd: "nim doc --doccmd:-d:testFooExternal --hints:off $file"
 action: "compile"
 nimout: '''
 foo1
@@ -66,6 +66,39 @@ when true: # issue #12746
         # specifying Error is culprit
         discard
 
+when true: # runnableExamples with rdoccmd
+  runnableExamples "-d:testFoo -d:testBar":
+    doAssert defined(testFoo) and defined(testBar)
+    doAssert defined(testFooExternal)
+  runnableExamples "-d:testFoo2":
+    doAssert defined(testFoo2)
+    doAssert not defined(testFoo) # doesn't get confused by other examples
+
+  ## all these syntaxes work too
+  runnableExamples("-d:testFoo2"): discard
+  runnableExamples(): discard
+  runnableExamples: discard
+  runnableExamples "-r:off": # issue #10731
+    doAssert false ## we compile only (-r:off), so this won't be run
+  runnableExamples "-b:js":
+    import std/compilesettings
+    proc startsWith*(s, prefix: cstring): bool {.noSideEffect, importjs: "#.startsWith(#)".}
+    doAssert querySetting(backend) == "js"
+  runnableExamples "-b:cpp":
+    static: doAssert defined(cpp)
+    type std_exception {.importcpp: "std::exception", header: "<exception>".} = object
+
+  proc fun2*() =
+    runnableExamples "-d:foo": discard # checks that it also works inside procs
+
+  when false: # future work
+    # passing non-string-litterals (for reuse)
+    const a = "-b:cpp"
+    runnableExamples(a): discard
+
+    # passing seq (to run with multiple compilation options)
+    runnableExamples(@["-b:cpp", "-b:js"]): discard
+
 # also check for runnableExamples at module scope
 runnableExamples:
   block: