diff --git a/Makefile.config.in b/Makefile.config.in index 19992fa20..de1ace921 100644 --- a/Makefile.config.in +++ b/Makefile.config.in @@ -19,6 +19,7 @@ LIBBROTLI_LIBS = @LIBBROTLI_LIBS@ LIBCURL_LIBS = @LIBCURL_LIBS@ LIBSECCOMP_LIBS = @LIBSECCOMP_LIBS@ LOWDOWN_LIBS = @LOWDOWN_LIBS@ +NIXDOC_LIBS = -lnix_doc OPENSSL_LIBS = @OPENSSL_LIBS@ PACKAGE_NAME = @PACKAGE_NAME@ PACKAGE_VERSION = @PACKAGE_VERSION@ diff --git a/doc/manual/rl-next/repl-doc-command.md b/doc/manual/rl-next/repl-doc-command.md new file mode 100644 index 000000000..84aaa0802 --- /dev/null +++ b/doc/manual/rl-next/repl-doc-command.md @@ -0,0 +1,13 @@ +--- +synopsis: Experimental REPL support for documentation comments using `:doc` +cls: 564 +--- + +Using `:doc` in the REPL now supports showing documentation comments when defined on a function. + +Previously this was only able to document builtins, however it now will show comments defined on a lambda as well. + +This support is experimental and relies on an embedded version of [nix-doc](https://github.com/lf-/nix-doc). + +The logic also supports limited Markdown formatting of doccomments and should easily support any [RFC 145](https://github.com/NixOS/rfcs/blob/master/rfcs/0145-doc-strings.md) +compatible documentation comments in addition to simple commented documentation. diff --git a/flake.nix b/flake.nix index 0f64050f4..9cfff8963 100644 --- a/flake.nix +++ b/flake.nix @@ -198,11 +198,14 @@ ''; }; + nix-doc = final.callPackage ./nix-doc/package.nix {}; + nix = final.callPackage ./package.nix { inherit versionSuffix fileset; stdenv = currentStdenv; boehmgc = final.boehmgc-nix; busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell; + nix-doc = final.nix-doc; }; }; diff --git a/maintainers/build-release-notes.py b/maintainers/build-release-notes.py index ba645d19a..00c91e5da 100644 --- a/maintainers/build-release-notes.py +++ b/maintainers/build-release-notes.py @@ -38,7 +38,7 @@ for p in paths: try: e = frontmatter.load(p) if 'synopsis' not in e.metadata: - raise Exception('missing synposis') + raise Exception('missing synopsis') unknownKeys = set(e.metadata.keys()) - set(('synopsis', 'cls', 'issues', 'prs', 'significance')) if unknownKeys: raise Exception('unknown keys', unknownKeys) diff --git a/meson.build b/meson.build index dadb01b9c..66c664282 100644 --- a/meson.build +++ b/meson.build @@ -219,6 +219,11 @@ deps += toml11 nlohmann_json = dependency('nlohmann_json', required : true) deps += nlohmann_json +# nix-doc is a Rust project provided via buildInputs and unfortunately doesn't have any way to be detected. +# Just declare it manually to resolve this. +nix_doc = declare_dependency(link_args : [ '-lnix_doc' ]) +deps += nix_doc + # # Build-time tools # diff --git a/nix-doc/.gitignore b/nix-doc/.gitignore new file mode 100644 index 000000000..c0d245929 --- /dev/null +++ b/nix-doc/.gitignore @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2024 Jade Lovelace +# +# SPDX-License-Identifier: BSD-2-Clause OR MIT + +/target +result diff --git a/nix-doc/Cargo.lock b/nix-doc/Cargo.lock new file mode 100644 index 000000000..6fc63004a --- /dev/null +++ b/nix-doc/Cargo.lock @@ -0,0 +1,161 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "cbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b6ad25ae296159fb0da12b970b2fe179b234584d7cd294c891e2bbb284466b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "dissimilar" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e3bdc80eee6e16b2b6b0f87fbc98c04bee3455e35174c0de1a125d0688c632" + +[[package]] +name = "expect-test" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d9eafeadd538e68fb28016364c9732d78e420b9ff8853fa5e4058861e9f8d3" +dependencies = [ + "dissimilar", + "once_cell", +] + +[[package]] +name = "nix-doc" +version = "0.0.1" +dependencies = [ + "expect-test", + "rnix", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rnix" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9b645f0edba447dbfc6473dd22999f46a1d00ab39e777a2713a1cf34a1597b" +dependencies = [ + "cbitset", + "rowan", +] + +[[package]] +name = "rowan" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea7cadf87a9d8432e85cb4eb86bd2e765ace60c24ef86e79084dcae5d1c5a19" +dependencies = [ + "rustc-hash", + "smol_str", + "text_unit", + "thin-dst", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "2.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "text_unit" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20431e104bfecc1a40872578dbc390e10290a0e9c35fffe3ce6f73c15a9dbfc2" + +[[package]] +name = "thin-dst" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c46be180f1af9673ebb27bc1235396f61ef6965b3fe0dbb2e624deb604f0e" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/nix-doc/Cargo.toml b/nix-doc/Cargo.toml new file mode 100644 index 000000000..21dc54d1b --- /dev/null +++ b/nix-doc/Cargo.toml @@ -0,0 +1,17 @@ +[package] +description = "Nix documentation grepping tool" +edition = "2018" +name = "nix-doc" +version = "0.0.1" +license = "BSD-2-Clause OR MIT" +homepage = "https://github.com/lf-/nix-doc" +repository = "https://github.com/lf-/nix-doc" + +[lib] +crate_type = ["staticlib"] + +[dependencies] +rnix = "0.8.0" + +[dev-dependencies] +expect-test = "1.1.0" diff --git a/nix-doc/package.nix b/nix-doc/package.nix new file mode 100644 index 000000000..70a49b9a0 --- /dev/null +++ b/nix-doc/package.nix @@ -0,0 +1,11 @@ +{ + rustPlatform, + lib +}: + +rustPlatform.buildRustPackage { + name = "nix-doc"; + + cargoHash = "sha256-HXL235loBJnRje7KaMCCCTighv6WNYRrZ/jgkAQbEY0="; + src = lib.cleanSource ./.; +} diff --git a/nix-doc/src/lib.rs b/nix-doc/src/lib.rs new file mode 100644 index 000000000..9c2e43f2f --- /dev/null +++ b/nix-doc/src/lib.rs @@ -0,0 +1,326 @@ +// SPDX-FileCopyrightText: 2024 Jade Lovelace +// +// SPDX-License-Identifier: BSD-2-Clause OR MIT + +//! library components of nix-doc +pub mod pprint; + +use crate::pprint::pprint_args; + +use rnix::types::{Lambda, TypedNode}; +use rnix::SyntaxKind::*; +use rnix::{NodeOrToken, SyntaxNode, TextUnit, WalkEvent}; + +use std::ffi::{CStr, CString}; +use std::fs; +use std::iter; +use std::os::raw::c_char; +use std::panic; + +use std::ptr; + +use std::{fmt::Display, str}; + +pub type Result = std::result::Result>; + +const DOC_INDENT: usize = 3; + +struct SearchResult { + /// Name of the function + identifier: String, + + /// Dedented documentation comments + doc: String, + + /// Parameter block for the function + param_block: String, +} + +fn find_pos(file: &str, line: usize, col: usize) -> usize { + let mut lines = 1; + let mut line_start = 0; + let mut it = file.chars().enumerate().peekable(); + while let Some((count, ch)) = it.next() { + if ch == '\n' || ch == '\r' { + lines += 1; + let addend = if ch == '\r' && it.peek().map(|x| x.1) == Some('\n') { + it.next(); + 1 + } else { + 0 + }; + line_start = count + addend; + } + + let col_diff = ((count as i32) - (line_start as i32)).abs() as usize; + if lines == line && col_diff == col { + return count; + } + } + unreachable!(); +} + +impl SearchResult { + fn format(&self, filename: P, line: usize) -> String { + format!( + "**Synopsis:** `{}` = {}\n\n{}\n\n# {}", + self.identifier.as_str(), + self.param_block, + indented(&self.doc, DOC_INDENT), + format!("{}:{}", filename, line).as_str(), + ) + } +} + +/// Emits a string `s` indented by `indent` spaces +fn indented(s: &str, indent: usize) -> String { + let indent_s = iter::repeat(' ').take(indent).collect::(); + s.split('\n') + .map(|line| indent_s.clone() + line) + .collect::>() + .join("\n") +} + +/// Cleans up a single line, erasing prefix single line comments but preserving indentation +fn cleanup_single_line<'a>(s: &'a str) -> &'a str { + let mut cmt_new_start = 0; + for (idx, ch) in s.char_indices() { + // if we find a character, save the byte position after it as our new string start + if ch == '#' || ch == '*' { + cmt_new_start = idx + 1; + break; + } + // if, instead, we are on a line with no starting comment characters, leave it alone as it + // will be handled by dedent later + if !ch.is_whitespace() { + break; + } + } + &s[cmt_new_start..] +} + +/// Erases indents in comments. This is *almost* a normal dedent function, but it starts by looking +/// at the second line if it can. +fn dedent_comment(s: &str) -> String { + let mut whitespaces = 0; + let mut lines = s.lines(); + let first = lines.next(); + + // scan for whitespace + for line in lines.chain(first) { + let line_whitespace = line.chars().take_while(|ch| ch.is_whitespace()).count(); + + if line_whitespace != line.len() { + // a non-whitespace line, perfect for taking whitespace off of + whitespaces = line_whitespace; + break; + } + } + + // maybe the first considered line we found was indented further, so let's look for more lines + // that might have a shorter indent. In the case of one line, do nothing. + for line in s.lines().skip(1) { + let line_whitespace = line.chars().take_while(|ch| ch.is_whitespace()).count(); + + if line_whitespace != line.len() { + whitespaces = line_whitespace.min(whitespaces); + } + } + + // delete up to `whitespaces` whitespace characters from each line and reconstitute the string + let mut out = String::new(); + for line in s.lines() { + let content_begin = line.find(|ch: char| !ch.is_whitespace()).unwrap_or(0); + out.push_str(&line[content_begin.min(whitespaces)..]); + out.push('\n'); + } + + out.truncate(out.trim_end_matches('\n').len()); + out +} + +/// Deletes whitespace and leading comment characters +/// +/// Oversight we are choosing to ignore: if you put # characters at the beginning of lines in a +/// multiline comment, they will be deleted. +fn cleanup_comments, I: DoubleEndedIterator>(comment: &mut I) -> String { + dedent_comment( + &comment + .rev() + .map(|small_comment| { + small_comment + .as_ref() + // space before multiline start + .trim_start() + // multiline starts + .trim_start_matches("/*") + // trailing so we can grab multiline end + .trim_end() + // multiline ends + .trim_end_matches("*/") + // extra space that was in the multiline + .trim() + .split('\n') + // erase single line comments and such + .map(cleanup_single_line) + .collect::>() + .join("\n") + }) + .collect::>() + .join("\n"), + ) +} + +/// Get the docs for a specific function +pub fn get_function_docs(filename: &str, line: usize, col: usize) -> Option { + let content = fs::read(filename).ok()?; + let decoded = str::from_utf8(&content).ok()?; + let pos = find_pos(&decoded, line, col); + let rowan_pos = TextUnit::from_usize(pos); + let tree = rnix::parse(decoded); + + let mut lambda = None; + for node in tree.node().preorder() { + match node { + WalkEvent::Enter(n) => { + if n.text_range().start() >= rowan_pos && n.kind() == NODE_LAMBDA { + lambda = Lambda::cast(n); + break; + } + } + WalkEvent::Leave(_) => (), + } + } + let lambda = lambda?; + let res = visit_lambda("func".to_string(), &lambda); + Some(res.format(filename, line)) +} + +fn visit_lambda(name: String, lambda: &Lambda) -> SearchResult { + // grab the arguments + let param_block = pprint_args(&lambda); + + // find the doc comment + let comment = find_comment(lambda.node().clone()).unwrap_or_else(|| "".to_string()); + + SearchResult { + identifier: name, + doc: comment, + param_block + } +} + +fn find_comment(node: SyntaxNode) -> Option { + let mut node = NodeOrToken::Node(node); + let mut comments = Vec::new(); + loop { + loop { + if let Some(new) = node.prev_sibling_or_token() { + node = new; + break; + } else { + node = NodeOrToken::Node(node.parent()?); + } + } + + match node.kind() { + TOKEN_COMMENT => match &node { + NodeOrToken::Token(token) => comments.push(token.text().clone()), + NodeOrToken::Node(_) => unreachable!(), + }, + // This stuff is found as part of `the-fn = f: ...` + // here: ^^^^^^^^ + NODE_KEY | TOKEN_ASSIGN => (), + t if t.is_trivia() => (), + _ => break, + } + } + let doc = cleanup_comments(&mut comments.iter().map(|c| c.as_str())); + Some(doc).filter(|it| !it.is_empty()) +} + +/// Get the docs for a function in the given file path at the given file position and return it as +/// a C string pointer +#[no_mangle] +pub extern "C" fn nd_get_function_docs( + filename: *const c_char, + line: usize, + col: usize, + ) -> *const c_char { + let fname = unsafe { CStr::from_ptr(filename) }; + fname + .to_str() + .ok() + .and_then(|f| { + panic::catch_unwind(|| get_function_docs(f, line, col)) + .map_err(|e| { + eprintln!("panic!! {:#?}", e); + e + }) + .ok() + }) + .flatten() + .and_then(|s| CString::new(s).ok()) + .map(|s| s.into_raw() as *const c_char) + .unwrap_or(ptr::null()) +} + +/// Call this to free a string from nd_get_function_docs +#[no_mangle] +pub extern "C" fn nd_free_string(s: *const c_char) { + unsafe { + // cast note: this cast is turning something that was cast to const + // back to mut + drop(CString::from_raw(s as *mut c_char)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bytepos() { + let fakefile = "abc\ndef\nghi"; + assert_eq!(find_pos(fakefile, 2, 2), 5); + } + + #[test] + fn test_bytepos_cursed() { + let fakefile = "abc\rdef\r\nghi"; + assert_eq!(find_pos(fakefile, 2, 2), 5); + assert_eq!(find_pos(fakefile, 3, 2), 10); + } + + #[test] + fn test_comment_stripping() { + let ex1 = ["/* blah blah blah\n foooo baaar\n blah */"]; + assert_eq!( + cleanup_comments(&mut ex1.iter()), + "blah blah blah\n foooo baaar\nblah" + ); + + let ex2 = ["# a1", "# a2", "# aa"]; + assert_eq!(cleanup_comments(&mut ex2.iter()), "aa\n a2\na1"); + } + + #[test] + fn test_dedent() { + let ex1 = "a\n b\n c\n d"; + assert_eq!(dedent_comment(ex1), "a\nb\nc\n d"); + let ex2 = "a\nb\nc"; + assert_eq!(dedent_comment(ex2), ex2); + let ex3 = " a\n b\n\n c"; + assert_eq!(dedent_comment(ex3), "a\nb\n\n c"); + } + + #[test] + fn test_single_line_comment_stripping() { + let ex1 = " * a"; + let ex2 = " # a"; + let ex3 = " a"; + assert_eq!(cleanup_single_line(ex1), " a"); + assert_eq!(cleanup_single_line(ex2), " a"); + assert_eq!(cleanup_single_line(ex3), ex3); + } +} diff --git a/nix-doc/src/pprint.rs b/nix-doc/src/pprint.rs new file mode 100644 index 000000000..7e73d2d20 --- /dev/null +++ b/nix-doc/src/pprint.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2024 Jade Lovelace +// +// SPDX-License-Identifier: BSD-2-Clause OR MIT + +use rnix::types::{Lambda, TypedNode}; +use rnix::SyntaxKind::*; + +/// Pretty-prints the arguments to a function +pub fn pprint_args(lambda: &Lambda) -> String { + // TODO: handle docs directly on NODE_IDENT args (uncommon case) + let mut lambda = lambda.clone(); + let mut out = String::new(); + loop { + let arg = lambda.arg().unwrap(); + match arg.kind() { + NODE_IDENT => { + out += &format!("*{}*", &arg.to_string()); + out.push_str(": "); + let body = lambda.body().unwrap(); + if body.kind() == NODE_LAMBDA { + lambda = Lambda::cast(body).unwrap(); + } else { + break; + } + } + NODE_PATTERN => { + out += &format!("*{}*", &arg.to_string()); + out.push_str(": "); + break; + } + t => { + unreachable!("unhandled arg type {:?}", t); + } + } + } + out.push_str("..."); + out + + //pprint_arg(lambda.arg()); +} diff --git a/package.nix b/package.nix index 1c943e046..ab08add16 100644 --- a/package.nix +++ b/package.nix @@ -43,6 +43,8 @@ busybox-sandbox-shell, + nix-doc, + pname ? "nix", versionSuffix ? "", officialRelease ? true, @@ -186,6 +188,7 @@ in stdenv.mkDerivation (finalAttrs: { lowdown libsodium toml11 + nix-doc ] ++ lib.optionals stdenv.hostPlatform.isLinux [ libseccomp busybox-sandbox-shell ] ++ lib.optional stdenv.hostPlatform.isx86_64 libcpuid diff --git a/src/libcmd/local.mk b/src/libcmd/local.mk index afd35af08..dcd33e84c 100644 --- a/src/libcmd/local.mk +++ b/src/libcmd/local.mk @@ -8,7 +8,7 @@ libcmd_SOURCES := $(wildcard $(d)/*.cc) libcmd_CXXFLAGS += -I src/libutil -I src/libstore -I src/libexpr -I src/libmain -I src/libfetchers -libcmd_LDFLAGS = $(EDITLINE_LIBS) $(LOWDOWN_LIBS) -pthread +libcmd_LDFLAGS = $(EDITLINE_LIBS) $(LOWDOWN_LIBS) $(NIXDOC_LIBS) -pthread libcmd_LIBS = libstore libutil libexpr libmain libfetchers diff --git a/src/libcmd/meson.build b/src/libcmd/meson.build index 6ef293c8f..167cb0f06 100644 --- a/src/libcmd/meson.build +++ b/src/libcmd/meson.build @@ -45,6 +45,7 @@ libcmd = library( editline, lowdown, nlohmann_json, + nix_doc ], install : true, # FIXME(Qyriad): is this right? diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index 45b56d012..0d7ad63a7 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -36,8 +36,26 @@ #include #endif +// XXX: These are for nix-doc features and will be removed in a future rewrite where this functionality is integrated more natively. +extern "C" { + char const *nd_get_function_docs(char const *filename, size_t line, size_t col); + void nd_free_string(char const *str); +} + namespace nix { + +/** Wrapper around std::unique_ptr with a custom deleter for strings from nix-doc **/ +using NdString = std::unique_ptr; + +/** + * Fetch a string representing the doc comment using nix-doc and wrap it in an RAII wrapper. + */ +NdString lambdaDocsForPos(SourcePath const path, nix::Pos const &pos) { + std::string const file = path.to_string(); + return NdString{nd_get_function_docs(file.c_str(), pos.line, pos.column), &nd_free_string}; +} + /** * Returned by `NixRepl::processLine`. */ @@ -400,7 +418,7 @@ ProcessLineResult NixRepl::processLine(std::string line) << " nix-shell\n" << " :t Describe result of evaluation\n" << " :u Build derivation, then start nix-shell\n" - << " :doc Show documentation of a builtin function\n" + << " :doc Show documentation for the provided function (experimental lambda support)\n" << " :log Show logs for a derivation\n" << " :te, :trace-enable [bool] Enable, disable or toggle showing traces for\n" << " errors\n" @@ -629,8 +647,24 @@ ProcessLineResult NixRepl::processLine(std::string line) markdown += stripIndentation(doc->doc); logger->cout(trim(renderMarkdownToTerminal(markdown))); - } else - throw Error("value does not have documentation"); + } else if (v.isLambda()) { + auto pos = state->positions[v.lambda.fun->pos]; + if (auto path = std::get_if(&pos.origin)) { + // Path and position have now been obtained, feed to nix-doc library to get data. + auto docComment = lambdaDocsForPos(*path, pos); + if (!docComment) { + throw Error("lambda '%s' has no documentation comment", pos); + } + + // Build and print Markdown representation of documentation comment. + std::string markdown = stripIndentation(docComment.get()); + logger->cout(trim(renderMarkdownToTerminal(markdown))); + } else { + throw Error("lambda '%s' doesn't have a determinable source file", pos); + } + } else { + throw Error("value '%s' does not have documentation", arg); + } } else if (command == ":te" || command == ":trace-enable") {