Skip to content

Conversation

@jjtolton
Copy link

@jjtolton jjtolton commented Nov 6, 2025

Summary

Update: After discussion and reading comments in the PR thread, I've been persuaded that -t is an overall more flexible and elegant solution. This PR now implements a custom toplevel flag instead of the original --halt-on-error and --always-halt flags.

This PR adds a -t GOAL flag 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

#!/bin/bash
scryer-prolog -g "undefined_predicate"
echo "This line may never execute"

Scenario 2: Programs without explicit halt drop into REPL

#!/bin/bash
scryer-prolog my_script.pl  # No halt/0 in script
echo "This line may never execute"

This is problematic in CI/CD pipelines, automated testing, cron jobs, Docker containers, and batch processing.

Solution: Custom Toplevel with -t

The -t flag 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 REPL

With goals:

scryer-prolog -g "run_tests" -t halt
# Runs goal, then halts instead of entering REPL

Custom exit codes:

% my_script.pl
my_toplevel :- 
    (run_tests -> halt(0) ; halt(1)).
scryer-prolog -t my_toplevel my_script.pl
# Custom logic for exit behavior

Why -t is Better

Compared to the original --halt-on-error / --always-halt approach:

  1. More flexible: Can define custom toplevel behavior, not just halt
  2. Prolog-idiomatic: Follows natural Prolog conventions for toplevel customization
  3. Simpler implementation: Single flag instead of two interdependent flags
  4. More powerful: Can implement complex exit logic in Prolog
  5. Backward compatible: Default behavior (REPL) unchanged when flag not used

Changes

Modified Files

  1. src/toplevel.pl

    • Added custom_toplevel/1 dynamic predicate
    • Added gather_toplevel/2 to process -t flag arguments
    • Modified start_toplevel/0 to check for custom toplevel or default to REPL
    • Updated help text to document -t flag
    • Fixed bug where -t argument was incorrectly processed as a filename
  2. src/tests/custom_toplevel.pl (new)

    • Integration tests for custom toplevel functionality
    • Tests for -t halt behavior
    • Tests for custom user-defined toplevels
  3. tests/scryer/cli/src_tests/custom_toplevel_tests.toml (new)

    • CLI test configuration
    • Verifies -t halt prevents REPL entry

Usage Examples

Basic Usage

# Normal behavior (enters REPL)
scryer-prolog my_file.pl

# Exit after loading without entering REPL
scryer-prolog -t halt my_file.pl

# Run goal and exit
scryer-prolog -g "write('Hello')" -t halt

In CI/CD Pipeline

#!/bin/bash
set -e

scryer-prolog -g "compile_project" -t halt
scryer-prolog -g "run_tests" -t halt
scryer-prolog -g "check_coverage" -t halt

echo "All checks passed!"

Custom Toplevel Logic

% test_runner.pl
custom_exit :-
    write('Running tests...'), nl,
    (run_all_tests ->
        write('✓ All tests passed'), nl,
        halt(0)
    ;
        write('✗ Tests failed'), nl,
        halt(1)
    ).
scryer-prolog -t custom_exit test_runner.pl

In Makefile

test:
	scryer-prolog -g "run_all_tests" -t halt

build:
	scryer-prolog -t halt build.pl

.PHONY: test build

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 halt exits cleanly without entering REPL
-t custom_predicate calls user-defined predicates
✅ Custom toplevels can set exit codes via halt/1
-g goals work correctly with -t flag
✅ Files are loaded before custom toplevel is invoked
✅ Bug fix: -t argument no longer incorrectly processed as filename

Related

Resolves #3146

Implementation Notes

The implementation uses a dynamic predicate custom_toplevel/1 that is set during argument processing. The start_toplevel/0 predicate checks for a custom toplevel and calls it using user: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

@jjtolton jjtolton force-pushed the error-termination-flag branch from 2402a21 to ccc2ff1 Compare November 6, 2025 02:43
@jjtolton jjtolton marked this pull request as ready for review November 6, 2025 03:48
jjtolton added a commit to jjtolton/scryer-prolog that referenced this pull request Nov 6, 2025
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>
@UWN
Copy link

UWN commented Nov 6, 2025

Does this mean that --always-halt masks errors?

@jjtolton
Copy link
Author

jjtolton commented Nov 6, 2025

Does this mean that --always-halt masks errors?

No, --always-halt is invoked only when the repl would be invoked. So, in the case of error, after the error was printed and it would drop into the repl. Otherwise, when the repl would normally be invoked after the other work of the process.

@triska
Copy link
Contributor

triska commented Nov 6, 2025

Please see @bakaq's comment and pointer about this: #3146 (comment)

@triska
Copy link
Contributor

triska commented Nov 7, 2025

Thank you a lot, can you please remove the now no longer needed intermediate commits?

@jjtolton jjtolton changed the title Add --halt-on-error flag for safer scripting Add -t flag for custom toplevel (replaces --halt-on-error) Nov 7, 2025
@jjtolton jjtolton force-pushed the error-termination-flag branch from 829bb22 to c69a352 Compare November 7, 2025 01:19
@jjtolton
Copy link
Author

jjtolton commented Nov 7, 2025

Thank you a lot, can you please remove the now no longer needed intermediate commits?

I don't mind but are we not squash merging?

@jjtolton jjtolton force-pushed the error-termination-flag branch from c69a352 to 006597d Compare November 7, 2025 04:02
jjtolton and others added 3 commits November 6, 2025 23:06
- Remove --halt-on-error and --always-halt flags and environment variables
- 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
- Remove halt_on_error and always_halt from MachineArgs struct

This is more elegant and flexible than the previous approach, allowing
users to define their own toplevel behavior including halt, custom REPLs,
or server loops.

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>
@jjtolton jjtolton force-pushed the error-termination-flag branch from 006597d to d56f604 Compare November 7, 2025 04:06
@triska
Copy link
Contributor

triska commented Nov 7, 2025

are we not squash merging?

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.

@Skgland
Copy link
Contributor

Skgland commented Nov 7, 2025

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).

@triska
Copy link
Contributor

triska commented Nov 7, 2025

@Skgland: Good point. I think a fact like g_caused_exception(Goal, Exception). could be asserted here:

write_term(Exception, [double_quotes(DQ)]), nl % halt?

@triska
Copy link
Contributor

triska commented Nov 7, 2025

Maybe a good topic for a separate PR?

@jjtolton
Copy link
Author

jjtolton commented Nov 8, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add --halt-on-error flag for safer scripting

4 participants