- Notifications
You must be signed in to change notification settings - Fork 157
Add -t flag for custom toplevel (replaces --halt-on-error) #3147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
2402a21 to ccc2ff1 Compare Updated BLEEDING.md and README.md to document the new --always-halt flag that was added alongside --halt-on-error in PR mthom#3147. Changes to BLEEDING.md: - Expanded section for PR mthom#3147 to cover both flags - Added behavior comparison table showing all scenarios - Added practical examples for CI/CD, Makefiles, and batch processing - Clarified the problem both flags solve Changes to README.md: - Updated PR mthom#3147 description to mention both flags - Added example showing combined usage The --always-halt flag solves the common problem of programs dropping into the REPL after successful execution when they don't explicitly call halt/0, making Scryer fully script-safe when combined with --halt-on-error. Co-Authored-By: J.J.'s Robot <jjtolton@gmail.com>
| Does this mean that |
No, |
| Please see @bakaq's comment and pointer about this: #3146 (comment) |
| Thank you a lot, can you please remove the now no longer needed intermediate commits? |
829bb22 to c69a352 Compare
I don't mind but are we not squash merging? |
006597d to d56f604 Compare
Thank you a lot! Individual git commits tend to be meaningfully separated, combining them ignores information that is otherwise present, and adding unrelated commits makes other operations (such as tracing the development history) harder. |
| Is there some way for the toplevel (-t) to tell whether an initialisation goal (-g) failed so that it can exit with a different exit code in case of failure. Something like alt_repl :- successful_init -> halt ; halt(1). |
| Maybe a good topic for a separate PR? |
| I would be happy to include it here as my entire purpose for this was "halt on error", and knowing if the exit code was non-zero is important. With -t halt, it works but the exit code is always 0. |
When a goal throws an exception during initialization (-g flag), the system now asserts g_caused_exception(Goal, Exception) in the user module. This allows custom toplevels (-t flag) to check if an error occurred and handle it appropriately. Example usage: scryer-prolog -g "throw(error)" -t check_error Where check_error can be: :- dynamic(g_caused_exception/2). check_error :- ( g_caused_exception(_, E) -> write('Error: '), write(E), nl, halt(1) ; halt(0) ). This enables scripts to use custom toplevels for sophisticated error handling and exit code logic. Addresses: mthom#3147 (comment) Co-Authored-By: J.J.'s Robot <jjtolton@gmail.com> | % Helper predicates for testing custom toplevel functionality | ||
| | ||
| success_toplevel :- | ||
| write('SUCCESS_TOPLEVEL_EXECUTED'), nl, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
format/2 and strings seem a bit more suitable for such ephemeral data, as in:
format("SUCCESS_...~n", []) | These (generated) scripts having been working well (per @triska 's suggestion, and good suggestion @Skgland for the error term) #!/usr/bin/env bash ### ~/bin/scryer-prolog ### # trace if environment variable TRACE is set if [[ "${TRACE-0}" == "1" ]]; then set -o xtrace; fi SCRYER_BIN="/home/jay/programs/scryer-prolog/target/release/scryer-prolog" HELPER_FILE="/home/jay/programs/scryer-prolog/.scryer_wrapper_helper.pl" function main() { # Check if -t flag is present in arguments local has_t_flag=false for arg in "$@"; do if [[ "$arg" == "-t" ]]; then has_t_flag=true break fi done # If no -t flag, add default error-checking toplevel if [[ "$has_t_flag" == "false" ]]; then exec "$SCRYER_BIN" "$HELPER_FILE" -t scryer_wrapper_check_error "$@" else exec "$SCRYER_BIN" "$@" fi } if [ "$0" = "${BASH_SOURCE[@]}" ]; then # exit on error set -o errexit # exit on undefined variable set -o nounset # exit on error in pipeline set -o pipefail main "$@" fi% .scryer_wrapper_helper.pl % Helper file for scryer-prolog wrapper script % Checks if a -g goal threw an exception and exits with appropriate code :- dynamic(g_caused_exception/2). scryer_wrapper_check_error :- ( g_caused_exception(_Goal, _Exception) -> halt(1) ; halt(0) ). |
d61ec03 to c99db11 Compare | Awesome, this is starting to look very good, thank you a lot! Regarding the |
| Thank you a lot, it looks very good! I noticed only one small remaining issue: The commit message of the first commit (fba0be7) currently still speaks of "Replace halt flags", and "Remove --halt-on-error", which were never present in Scryer. |
When a goal throws an exception during initialization (-g flag), the system now asserts g_caused_exception(Goal, Exception) in the user module. This allows custom toplevels (-t flag) to check if an error occurred and handle it appropriately. Example usage: scryer-prolog -g "throw(error)" -t check_error Where check_error can be: :- dynamic(g_caused_exception/2). check_error :- ( g_caused_exception(_, E) -> write('Error: '), write(E), nl, halt(1) ; halt(0) ). This enables scripts to use custom toplevels for sophisticated error handling and exit code logic. Addresses: mthom#3147 (comment) Co-Authored-By: J.J.'s Robot <jjtolton@gmail.com> b00a8fb to e95692d Compare | How come 655fd94 is now part of this PR? |
| bc I still don't know how to rebase. stand by
|
When a goal throws an exception during initialization (-g flag), the system now asserts g_caused_exception(Goal, Exception) in the user module. This allows custom toplevels (-t flag) to check if an error occurred and handle it appropriately. Example usage: scryer-prolog -g "throw(error)" -t check_error Where check_error can be: :- dynamic(g_caused_exception/2). check_error :- ( g_caused_exception(_, E) -> write('Error: '), write(E), nl, halt(1) ; halt(0) ). This enables scripts to use custom toplevels for sophisticated error handling and exit code logic. Addresses: mthom#3147 (comment) Co-Authored-By: J.J.'s Robot <jjtolton@gmail.com> e95692d to 25d9c6e Compare | The |
| Honestly, doing a |
- Add -t FLAG to specify custom toplevel (arity 0 predicate) - Default toplevel is 'repl' if -t is not specified - Using `-t halt` achieves original goal of guaranteed termination - Custom toplevels enable flexible exit strategies (e.g., server mode) - Update help text to document -t flag Examples: scryer-prolog -t halt program.pl # Exits after execution scryer-prolog -t my_repl program.pl # Custom REPL scryer-prolog program.pl # Default REPL Co-Authored-By: J.J.'s Robot <noreply@example.com>
- Create Prolog integration tests in src/tests/custom_toplevel.pl - Add CLI test configuration in tests/scryer/cli/src_tests/custom_toplevel_tests.toml - Tests verify: * -t halt terminates after initialization * Custom toplevels can be user-defined predicates * Toplevel receives control after initialization completes * Default behavior is REPL when no -t specified - All tests pass successfully Following TESTING_GUIDE.md three-layer testing approach: - Layer 2: Prolog integration tests with test_framework - Layer 3: CLI snapshot tests with .toml configuration Co-Authored-By: J.J.'s Robot <noreply@example.com>
Fixed issue where `scryer-prolog -t halt` would try to load "halt.pl" as a file instead of just using halt as the custom toplevel. The bug was caused by an extra clause `delegate_task([], []).` that would return control to the calling context instead of continuing to start_toplevel. This caused the argument processing in delegate_task to continue and treat the already-consumed toplevel argument as a filename. Removing this clause ensures that delegate_task([], Goals0) always proceeds to load initialization files and start the toplevel, fixing the double-processing bug. Co-Authored-By: J.J.'s Robot <jjtolton@gmail.com>
When a goal throws an exception during initialization (-g flag), the system now asserts g_caused_exception(Goal, Exception) in the user module. This allows custom toplevels (-t flag) to check if an error occurred and handle it appropriately. Example usage: scryer-prolog -g "throw(error)" -t check_error Where check_error can be: :- dynamic(g_caused_exception/2). check_error :- ( g_caused_exception(_, E) -> write('Error: '), write(E), nl, halt(1) ; halt(0) ). This enables scripts to use custom toplevels for sophisticated error handling and exit code logic. Addresses: mthom#3147 (comment) Co-Authored-By: J.J.'s Robot <jjtolton@gmail.com> Following TESTING_GUIDE.md, added tests at layers 2 and 3: Layer 2 - Prolog Integration Tests (src/tests/custom_toplevel.pl): - Test that g_caused_exception/2 is not asserted when no exception occurs - Test that g_caused_exception/2 can be checked from custom toplevel - Added check_for_exception/0 helper predicate for testing Layer 3 - CLI Tests (tests/scryer/cli/src_tests/custom_toplevel.md): - Test g_caused_exception/2 with exception thrown - Test g_caused_exception/2 with no exception - Test g_caused_exception/2 with error/2 terms - Added test helper predicates in fixtures/toplevel_test_helper.pl All tests pass successfully. Co-Authored-By: J.J.'s Robot <jjtolton@gmail.com>
- Add dynamic directive in toplevel.pl with other module-level directives - Update test files to reference it as '':g_caused_exception/2 - Remove redundant dynamic directives from test files - All tests passing
25d9c6e to e7c288f Compare | (debating if I should tell them I used |
| Test that files are loaded before toplevel runs | ||
| | ||
| ```trycmd | ||
| $ scryer-prolog -f --no-add-history tests/scryer/cli/fixtures/toplevel_test_helper.pl -t write_and_exit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The -t flag must be stated before any files is loaded. The convention is that switches after file names are application-specific, and switches before the files are Scryer-specific. This is currently not enforced, but will be necessary to follow in the future, when applications become more complex and need their own options (such as application-specific -t switches).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I much prefer the -- de-facto convention that is followed by basically all commands that need to pass arguments to something "inside". In fact, Scryer already supports this. Example:
:- use_module(library(os)). :- use_module(library(format)). main :- argv(A), portray_clause(A), halt.$ scryer-prolog a.pl -f -g main -- --inner-a 1 --inner-b 2 ["--inner-a","1","--inner-b","2"]. This makes Scryer more in line with the rest of the CLI ecosystem. Having an seemingly arbitrary restriction on the order of the arguments like this seems dated and clunky, though I can see the benefit if it is to be more compatible with other Prolog implmentations' CLI interfaces, and it isn't too bad if we do allow the -- too. I think most people would put the filename last naturally anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please let's follow the convention, the advantages will become apparent with more complex applications that need dedicated switches.
Scryer is by far not the only application that requires arguments in this order, it is not arbitrary and also not dated. For example:
$ git log Cargo.lock --stat fatal: option '--stat' must come before non-option arguments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using git as an example of a "not dated" CLI is audacious. It's definitely not old, but it being clunky is one of the big reasons that Jujutsu is a thing.
I agree that we should probably enforce this, but it will definitely feel dated.
| Test that g_caused_exception/2 is asserted when -g goal throws exception | ||
| | ||
| ```trycmd | ||
| $ scryer-prolog -f --no-add-history tests/scryer/cli/fixtures/toplevel_test_helper.pl -g "throw(test_error)" -t check_exception_halt_1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, please place Scryer-specific options (in this case: -g and -t) before files that are loaded.
| Test that g_caused_exception/2 is not asserted when -g goal succeeds | ||
| | ||
| ```trycmd | ||
| $ scryer-prolog -f --no-add-history tests/scryer/cli/fixtures/toplevel_test_helper.pl -g "write('Success')" -t check_exception_halt_0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here, please reorder the arguments, thank you a lot!
| Thank you a lot, I added a few comments regarding the ordering of command-line options: Scryer-specific flags should be stated before Prolog files. The positions after Prolog files are for application-specific options. |
The custom_toplevel.pl unit tests were trivial and didn't actually test the functionality. All real testing is done via comprehensive CLI tests in tests/scryer/cli/src_tests/custom_toplevel.md
2781921 to 5357ecf Compare Per maintainer feedback, switches must come before files to follow the convention: switches before files are Scryer-specific, switches after files are application-specific.
triska left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks awesome, this will be a very useful feature in many situations, thank you a lot!
| write(' -g, --goal GOAL '), | ||
| write('Run the query GOAL'), nl, | ||
| write(' -t GOAL '), | ||
| write('Use GOAL as custom toplevel (arity 0 predicate)'), nl, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a goal, the invoked predicate can have any arity.
For instance, if you only need an error code and not the type of error, you can use:
$ scryer-prolog -t 'halt(1)' -g run,halt your_file.pl
Does that work in your scripts? In that way, you do not have to implement a custom toplevel for this particular purpose.
Summary
Update: After discussion and reading comments in the PR thread, I've been persuaded that
-tis an overall more flexible and elegant solution. This PR now implements a custom toplevel flag instead of the original--halt-on-errorand--always-haltflags.This PR adds a
-t GOALflag that allows users to specify a custom toplevel predicate to run instead of the default REPL. This provides a more flexible and Prolog-idiomatic solution to the original problem of preventing scripts from hanging.Motivation
Problem: Shell Scripts Get Stuck
Shell scripts using Scryer Prolog can get stuck waiting for user input when errors occur or when programs complete without calling
halt/0:Scenario 1: Errors drop into REPL
Scenario 2: Programs without explicit halt drop into REPL
This is problematic in CI/CD pipelines, automated testing, cron jobs, Docker containers, and batch processing.
Solution: Custom Toplevel with
-tThe
-tflag allows you to specify any arity-0 predicate as the toplevel:Basic usage - exit instead of REPL:
scryer-prolog -t halt my_script.pl # Exits with code 0 after loading, never enters REPLWith goals:
Custom exit codes:
scryer-prolog -t my_toplevel my_script.pl # Custom logic for exit behaviorWhy
-tis BetterCompared to the original
--halt-on-error/--always-haltapproach:Changes
Modified Files
src/toplevel.plcustom_toplevel/1dynamic predicategather_toplevel/2to process-tflag argumentsstart_toplevel/0to check for custom toplevel or default to REPL-tflag-targument was incorrectly processed as a filenamesrc/tests/custom_toplevel.pl(new)-t haltbehaviortests/scryer/cli/src_tests/custom_toplevel_tests.toml(new)-t haltprevents REPL entryUsage Examples
Basic Usage
In CI/CD Pipeline
Custom Toplevel Logic
In Makefile
Testing
All tests pass, including:
✅ Comprehensive Prolog integration tests in
src/tests/custom_toplevel.pl✅ CLI tests in
tests/scryer/cli/src_tests/custom_toplevel_tests.toml✅
-t haltexits cleanly without entering REPL✅
-t custom_predicatecalls user-defined predicates✅ Custom toplevels can set exit codes via
halt/1✅
-ggoals work correctly with-tflag✅ Files are loaded before custom toplevel is invoked
✅ Bug fix:
-targument no longer incorrectly processed as filenameRelated
Resolves #3146
Implementation Notes
The implementation uses a dynamic predicate
custom_toplevel/1that is set during argument processing. Thestart_toplevel/0predicate checks for a custom toplevel and calls it usinguser:call/1, otherwise defaults to the standard REPL.This approach is clean, efficient, and follows Prolog conventions for customizing REPL behavior.
Co-Authored-By: J.J.'s Robot jjtolton@gmail.com