From cb023b9946237d6c147e8dcd67198dff16ebcd87 Mon Sep 17 00:00:00 2001 From: "Todd V. Jonker" Date: Tue, 31 Mar 2026 13:14:14 -0700 Subject: [PATCH 1/2] Replace `help` syntax form with `,doc` REPL command. Known issue: Documentation isn't located for bindings `define`d in the current namespace, and I'm not sure why the current code doesn't work. However, this may not be a meaningful issue, and it has not worked for at least as long as we've had Gradle builds. --- fusioncli/build.gradle.kts | 2 +- .../modules/fusion/private/cli/repl.fusion | 13 +- .../dev/ionfusion/fusioncli/repl/RepLoop.java | 10 +- .../ionfusion/fusioncli/repl/ReplContext.java | 10 +- .../ionfusion/fusioncli/repl/cmd/DocCmd.java | 130 ++++++++++++++ .../ionfusion/fusioncli/repl/cmd/ExitCmd.java | 2 +- .../fusioncli/repl/cmd/ReplHelpCmd.java | 2 +- .../ionfusion/fusioncli/repl/ReplDocTest.java | 65 +++++++ .../ionfusion/fusioncli/repl/ReplTest.java | 11 -- .../ionfusion/fusion/_Private_HelpForm.java | 166 ------------------ 10 files changed, 217 insertions(+), 194 deletions(-) create mode 100644 fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/DocCmd.java create mode 100644 fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplDocTest.java delete mode 100644 runtime/src/main/java/dev/ionfusion/fusion/_Private_HelpForm.java diff --git a/fusioncli/build.gradle.kts b/fusioncli/build.gradle.kts index 71629a34b..c1f35648a 100644 --- a/fusioncli/build.gradle.kts +++ b/fusioncli/build.gradle.kts @@ -33,7 +33,7 @@ tasks.jacocoTestCoverageVerification { violationRules { rule { limit { - minimum = "0.40".toBigDecimal() + minimum = "0.45".toBigDecimal() } } } diff --git a/fusioncli/src/main/fusion/modules/fusion/private/cli/repl.fusion b/fusioncli/src/main/fusion/modules/fusion/private/cli/repl.fusion index 475e409c3..b7deaa3ce 100644 --- a/fusioncli/src/main/fusion/modules/fusion/private/cli/repl.fusion +++ b/fusioncli/src/main/fusion/modules/fusion/private/cli/repl.fusion @@ -7,18 +7,11 @@ Helper forms for the REPL. ''' - (require "/fusion/ffi/java") + (require + "/fusion/private/syntax") (provide - help + quote_syntax // Needed by ,doc ) - (define help - ''' - (help ident ...) - -Prints documentation for the given bindings, if available. - ''' - (java_new "dev.ionfusion.fusion._Private_HelpForm")) - ) diff --git a/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/RepLoop.java b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/RepLoop.java index 03680df3c..c133ce409 100644 --- a/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/RepLoop.java +++ b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/RepLoop.java @@ -7,6 +7,7 @@ import com.amazon.ion.IonException; import dev.ionfusion.fusioncli.framework.CommandSuite; +import dev.ionfusion.fusioncli.repl.cmd.DocCmd; import dev.ionfusion.fusioncli.repl.cmd.ExitCmd; import dev.ionfusion.fusioncli.repl.cmd.ReplHelpCmd; import dev.ionfusion.runtime.base.FusionException; @@ -34,8 +35,10 @@ public abstract class RepLoop myOut = stdout; CommandSuite commands = new CommandSuite(new ExitCmd(), - new ReplHelpCmd()); - ReplContext context = new ReplContext(commands, stdout); + new ReplHelpCmd(), + new DocCmd()); + ReplContext context = new ReplContext(commands, myTopLevel, stdout); + myCli = new ReplCli(context); } @@ -70,7 +73,8 @@ private void welcome() red("\nWelcome to Fusion!\n\n"); myOut.println("Type..."); myOut.println(" ,exit to exit. ^D should work too."); - myOut.println(" ,help for see all REPL commands. Try `,help help`!"); + myOut.println(" ,doc to view documentation for a Fusion feature"); + myOut.println(" ,help to view a list of more REPL commands. Try `,help help`!"); myOut.println(); } diff --git a/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/ReplContext.java b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/ReplContext.java index 72eb860d4..e89050daa 100644 --- a/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/ReplContext.java +++ b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/ReplContext.java @@ -5,20 +5,28 @@ import dev.ionfusion.fusioncli.framework.CommandContext; import dev.ionfusion.fusioncli.framework.CommandSuite; +import dev.ionfusion.runtime.embed.TopLevel; import java.io.PrintWriter; public class ReplContext extends CommandContext { + private final TopLevel myTopLevel; protected final PrintWriter myOut; - public ReplContext(CommandSuite suite, PrintWriter out) + public ReplContext(CommandSuite suite, TopLevel topLevel, PrintWriter out) { super(suite); + myTopLevel = topLevel; myOut = out; } + public TopLevel top() + { + return myTopLevel; + } + public PrintWriter out() { return myOut; diff --git a/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/DocCmd.java b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/DocCmd.java new file mode 100644 index 000000000..17c674ccc --- /dev/null +++ b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/DocCmd.java @@ -0,0 +1,130 @@ +// Copyright Ion Fusion contributors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package dev.ionfusion.fusioncli.repl.cmd; + +import static dev.ionfusion.fusion.FusionSyntax.isIdentifier; +import static dev.ionfusion.fusion._Private_Trampoline.findBindingDoc; + +import dev.ionfusion.fusioncli.framework.Command; +import dev.ionfusion.fusioncli.framework.Executor; +import dev.ionfusion.fusioncli.framework.UsageException; +import dev.ionfusion.fusioncli.repl.ReplContext; +import dev.ionfusion.fusioncli.repl.ReplExecutor; +import dev.ionfusion.runtime._private.doc.BindingDoc; +import dev.ionfusion.runtime.base.FusionException; +import dev.ionfusion.runtime.embed.TopLevel; +import java.io.PrintWriter; + +public class DocCmd + extends Command +{ + private static final String HELP_ONE_LINER = + "Print documentation for a given identifier"; + + private static final String HELP_USAGE = + "doc IDENTIFIER"; + + private static final String HELP_BODY = + "Resolves the given identifier in the current namespace and prints any associated\n" + + "documentation."; + + + public DocCmd() + { + super("doc"); + putHelpText(HELP_ONE_LINER, HELP_USAGE, HELP_BODY); + } + + @Override + public Executor makeExecutor(ReplContext replContext, String[] args) + throws UsageException + { + assert args.length < 2; + if (args.length == 0) + { + throw usage("Expected an identifier"); + } + + return new DocExecutor(replContext, args[0]); + } + + + private class DocExecutor + extends ReplExecutor + { + private final String myArg; + + private DocExecutor(ReplContext replContext, String arg) + { + super(replContext); + myArg = arg; + } + + @Override + public int execute() + throws Exception + { + Object id = determineIdentifier(); + displayDoc(id); + + return 0; + } + + private Object determineIdentifier() + throws Exception + { + TopLevel top = context().top(); + + // TODO This assumes the normal binding of `quote_syntax` + + Object stx; + try + { + stx = top.eval("(quote_syntax " + myArg + ")"); + } + catch (FusionException e) + { + throw usage("Expected an identifier"); + } + + if (! isIdentifier(top, stx)) + { + throw usage("Expected an identifier"); + } + + return stx; + } + + private void displayDoc(Object id) + { + PrintWriter out = context().out(); + + BindingDoc doc = findBindingDoc(context().top(), id); + if (doc == null) + { + out.println("No documentation available."); + return; + } + + if (doc.getKind() != null) + { + out.append("["); + // Using enum toString() allows display name to be changed + out.append(doc.getKind().toString()); + out.append("] "); + } + if (doc.getUsage() != null) + { + out.append(doc.getUsage()); + } + + if (doc.getBody() != null) + { + out.append('\n'); + out.append(doc.getBody()); + out.append('\n'); + } + } + } +} diff --git a/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/ExitCmd.java b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/ExitCmd.java index 4590334d3..d65aae771 100644 --- a/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/ExitCmd.java +++ b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/ExitCmd.java @@ -11,7 +11,7 @@ public class ExitCmd extends Command { - static final String HELP_ONE_LINER = + private static final String HELP_ONE_LINER = "Exit the REPL"; private static final String HELP_USAGE = diff --git a/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/ReplHelpCmd.java b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/ReplHelpCmd.java index f4cf9f5fa..0cf50b26b 100644 --- a/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/ReplHelpCmd.java +++ b/fusioncli/src/main/java/dev/ionfusion/fusioncli/repl/cmd/ReplHelpCmd.java @@ -15,7 +15,7 @@ public class ReplHelpCmd extends Command { - static final String HELP_ONE_LINER = + private static final String HELP_ONE_LINER = "Describe available REPL commands"; private static final String HELP_USAGE = diff --git a/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplDocTest.java b/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplDocTest.java new file mode 100644 index 000000000..bc96cfcee --- /dev/null +++ b/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplDocTest.java @@ -0,0 +1,65 @@ +// Copyright Ion Fusion contributors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package dev.ionfusion.fusioncli.repl; + +import org.junit.jupiter.api.Test; + +public class ReplDocTest + extends ReplTestCase +{ + @Test + public void docWithoutArg() + throws Exception + { + supplyInput(",doc\n"); + runRepl(); + + expectError("Expected an identifier"); + } + + + @Test + public void docForUnbound() + throws Exception + { + supplyInput(",doc unbound_id\n"); + runRepl(); + + // TODO Distinguish between unbound vars and undocumented bindings. + expectError("No documentation available."); + } + + + @Test + public void docForBuiltin() + throws Exception + { + supplyInput(",doc * \n"); // Extra space is intentional. + runRepl(); + + expectResponse("Returns the product"); + } + + + @Test + public void docForBadSyntax() + throws Exception + { + supplyInput(",doc [\n"); // Extra space is intentional. + runRepl(); + + expectError("Expected an identifier"); + } + + + @Test + public void docForNonIdentifier() + throws Exception + { + supplyInput(",doc 12\n"); // Extra space is intentional. + runRepl(); + + expectError("Expected an identifier"); + } +} diff --git a/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplTest.java b/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplTest.java index 12f28e43d..f2b315e2c 100644 --- a/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplTest.java +++ b/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplTest.java @@ -69,17 +69,6 @@ public void testFusionSyntaxError() } - @Test - public void testHelpHelp() - throws Exception - { - supplyInput("(help help)\n"); - runRepl(); - - expectResponse("(help ident ...)"); - } - - //================================================================================== // Basic comma-commands diff --git a/runtime/src/main/java/dev/ionfusion/fusion/_Private_HelpForm.java b/runtime/src/main/java/dev/ionfusion/fusion/_Private_HelpForm.java deleted file mode 100644 index 631b34270..000000000 --- a/runtime/src/main/java/dev/ionfusion/fusion/_Private_HelpForm.java +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright Ion Fusion contributors. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package dev.ionfusion.fusion; - -import static dev.ionfusion.fusion.FusionSexp.unsafePairTail; - -import dev.ionfusion.fusion.FusionSexp.BaseSexp; -import dev.ionfusion.fusion.ModuleNamespace.CompiledImportedVariableReference; -import dev.ionfusion.fusion.TopLevelNamespace.CompiledTopLevelVariableReference; -import dev.ionfusion.runtime._private.doc.BindingDoc; -import dev.ionfusion.runtime.base.FusionException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - - -/** - * This is ugly, ugly, ugly. - */ -public final class _Private_HelpForm - extends SyntacticForm -{ - private static final class HelpDocument - extends BaseValue - { - private final List myArgs; - - private HelpDocument(List args) - { - myArgs = args; - } - - @Override - public void write(Evaluator eval, Appendable out) - throws IOException - { - for (BindingDoc doc : myArgs) - { - if (doc == null) - { - out.append("\nNo documentation available.\n"); - } - else - { - if (doc.getKind() != null) - { - out.append("\n["); - // Using enum toString() allows display name to be changed - out.append(doc.getKind().toString()); - out.append("] "); - } - if (doc.getUsage() != null) - { - out.append(doc.getUsage()); - } - out.append('\n'); - - if (doc.getBody() != null) - { - out.append('\n'); - out.append(doc.getBody()); - out.append('\n'); - } - } - } - } - } - - - @Override - SyntaxValue expand(Expander expander, Environment env, SyntaxSexp stx) - throws FusionException - { - // TODO reject if not at top level - final Evaluator eval = expander.getEvaluator(); - - SyntaxChecker check = check(eval, stx); - int arity = stx.size(eval); - - SyntaxValue[] children = stx.extract(eval); - - // Expand (help) into (help help) - if (arity == 1) - { - children = Arrays.copyOf(children, 2); - children[1] = children[0]; - } - - // Just make sure we've got a list of identifiers - for (int i = 1; i < arity; i++) - { - SyntaxSymbol identifier = check.requiredIdentifier(i); - - // We don't want to expand the identifier since it might be syntax - // and that will fail. But we do want to determine its binding so - // we can look up documentation at runtime. This may resolve to a - // FreeBinding, which will trigger an unbound-identifier error - // during compilation, which is appropriate. - - identifier.resolve(); - } - - return stx.copyReplacingChildren(eval, children); - } - - - @Override - CompiledForm compile(Compiler comp, Environment env, SyntaxSexp stx) - throws FusionException - { - Evaluator eval = comp.getEvaluator(); - BaseSexp forms = (BaseSexp) unsafePairTail(eval, stx.unwrap(eval)); - - CompiledForm[] children = comp.compileExpressions(env, forms); - return new CompiledHelp(children); - } - - private static final class CompiledHelp - implements CompiledForm - { - private final CompiledForm[] myChildren; - - private CompiledHelp(CompiledForm[] children) - { - myChildren = children; - } - - @Override - public Object doEval(Evaluator eval, Store store) - throws FusionException - { - ArrayList docs = new ArrayList<>(); - - for (CompiledForm form : myChildren) - { - BindingDoc doc = null; - if (form instanceof CompiledImportedVariableReference) - { - CompiledImportedVariableReference ref = - (CompiledImportedVariableReference) form; - NamespaceStore ns = store.namespace(); - ModuleStore module = - ns.lookupRequiredModule(ref.myModuleAddress); - doc = module.document(ref.myBindingAddress); - } - else if (form instanceof CompiledTopLevelVariableReference) - { - CompiledTopLevelVariableReference ref = - (CompiledTopLevelVariableReference) form; - Namespace ns = (Namespace) store.namespace(); - doc = ns.document(ref.myAddress); - } - - if (doc != null) - { - docs.add(doc); - } - } - - // TODO write directly to current_output_port or somesuch. - return new HelpDocument(docs); - } - } -} From 4b3bf6fe594cf61b16a60eb2d2a70fa64a6b88bd Mon Sep 17 00:00:00 2001 From: "Todd V. Jonker" Date: Tue, 31 Mar 2026 13:59:15 -0700 Subject: [PATCH 2/2] Add disabled unit test w/link to new issue --- .../dev/ionfusion/fusioncli/repl/ReplDocTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplDocTest.java b/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplDocTest.java index bc96cfcee..49d607c1d 100644 --- a/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplDocTest.java +++ b/fusioncli/src/test/java/dev/ionfusion/fusioncli/repl/ReplDocTest.java @@ -3,6 +3,7 @@ package dev.ionfusion.fusioncli.repl; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; public class ReplDocTest @@ -41,6 +42,17 @@ public void docForBuiltin() expectResponse("Returns the product"); } + @Test + @Disabled("https://github.com/ion-fusion/fusion-java/issues/510") + public void docForLocal() + throws Exception + { + supplyInput("(define (local) '''local docs''' true)\n"); + supplyInput(",doc local\n"); + runRepl(); + + expectResponse("local docs"); + } @Test public void docForBadSyntax()