Skip to content
35 changes: 31 additions & 4 deletions src/toplevel.pl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

:- dynamic(disabled_init_file/0).
:- dynamic(started/0).
:- dynamic(custom_toplevel/1).
:- dynamic(g_caused_exception/2).

load_scryerrc :-
( '$home_directory'(HomeDir) ->
Expand Down Expand Up @@ -53,7 +55,18 @@
; true
),
(\+ disabled_init_file -> load_scryerrc ; true),
repl.
start_toplevel.

start_toplevel :-
( custom_toplevel(Goal) ->
catch(user:call(Goal),
Exception,
( print_exception(Exception),
halt(1)
)
)
; repl
).

args_consults_goals([], [], []).
args_consults_goals([Arg|Args], Consults, Goals) :-
Expand All @@ -64,19 +77,19 @@
arg_consults_goals(g(Goal), Args, Consults, [g(Goal)|Goals]) :-
args_consults_goals(Args, Consults, Goals).

delegate_task([], []).
delegate_task([], Goals0) :-
(\+ disabled_init_file -> load_scryerrc ; true),
reverse(Goals0, Goals1),
args_consults_goals(Goals1, Consults, Goals),
run_goals(Consults),
run_goals(Goals),
repl.
start_toplevel.

delegate_task([Arg0|Args], Goals0) :-
( ( member(Arg0, ["-h", "--help"]) -> print_help
; member(Arg0, ["-v", "--version"]) -> print_version
; member(Arg0, ["-g", "--goal"]) -> gather_goal(g, Args, Goals0)
; member(Arg0, ["-t"]) -> gather_toplevel(Args, Goals0)
; member(Arg0, ["-f"]) -> disable_init_file
; member(Arg0, ["--no-add-history"]) -> ignore_machine_arg
),
Expand All @@ -96,6 +109,8 @@
write('Print version information and exit'), nl,
write(' -g, --goal GOAL '),
write('Run the query GOAL'), nl,
write(' -t GOAL '),
write('Use GOAL as custom toplevel (arity 0 predicate)'), nl,
Copy link
Contributor

@triska triska Nov 10, 2025

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.

write(' -f '),
write('Fast startup. Do not load initialization file (~/.scryerrc)'), nl,
write(' --no-add-history '),
Expand All @@ -117,6 +132,17 @@
Gs =.. [Type, Gs1],
delegate_task(Args, [Gs|Goals]).

gather_toplevel(Args0, Goals0) :-
length(Args0, N),
( N < 1 -> print_help, halt
; true
),
[TopLevel|Args] = Args0,
atom_chars(Goal, TopLevel),
retractall(custom_toplevel(_)),
asserta(custom_toplevel(Goal)),
delegate_task(Args, Goals0).

disable_init_file :-
asserta('disabled_init_file').

Expand Down Expand Up @@ -154,7 +180,8 @@
Exception,
( write_term(Goal, [variable_names(VNs),double_quotes(DQ)]),
write(' causes: '),
write_term(Exception, [double_quotes(DQ)]), nl % halt?
write_term(Exception, [double_quotes(DQ)]), nl,
asserta(g_caused_exception(Goal, Exception))
)
) -> true
; write('% Warning: initialization failed for: '),
Expand Down
48 changes: 48 additions & 0 deletions tests/scryer/cli/fixtures/toplevel_test_helper.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
% Helper predicates for testing custom toplevel functionality

success_toplevel :-
format("SUCCESS_TOPLEVEL_EXECUTED~n", []),
halt(0).

failure_toplevel :-
format("FAILURE_TOPLEVEL_EXECUTED~n", []),
halt(1).

exit_code_42 :-
format("EXIT_CODE_42~n", []),
halt(42).

write_and_exit :-
format("Output from custom toplevel~n", []),
halt(0).

% This one doesn't halt - to test what happens if toplevel doesn't halt
non_halting_toplevel :-
format("NON_HALTING_TOPLEVEL~n", []).

% Test that toplevel can access loaded predicates
test_file_loaded :-
format("LOADED_PREDICATE_CALLED~n", []),
halt(0).

helper_predicate :-
format("Helper predicate works~n", []).

% g_caused_exception/2 testing predicates
check_exception_halt_1 :-
( '$toplevel':g_caused_exception(Goal, Exception) ->
format("EXCEPTION_CAUGHT~n", []),
format("Goal: ~w~n", [Goal]),
format("Exception: ~w~n", [Exception]),
halt(1)
; format("NO_EXCEPTION~n", []),
halt(0)
).

check_exception_halt_0 :-
( '$toplevel':g_caused_exception(_, _) ->
format("UNEXPECTED_EXCEPTION~n", []),
halt(1)
; format("SUCCESS_NO_EXCEPTION~n", []),
halt(0)
).
125 changes: 125 additions & 0 deletions tests/scryer/cli/src_tests/custom_toplevel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Custom Toplevel Tests

## Basic -t halt functionality
Test that -t halt prevents entering REPL and exits cleanly

```trycmd
$ scryer-prolog -f --no-add-history -t halt
```

## -t halt with successful goal
Test that -t halt exits after running a goal successfully

```trycmd
$ scryer-prolog -f --no-add-history -g "write('Goal executed')" -t halt
Goal executed
```

## -t halt with failing goal
Test that -t halt still exits even when goal fails

```trycmd
$ scryer-prolog -f --no-add-history -g "fail" -t halt
% Warning: initialization failed for: fail

```

## Custom toplevel with exit code 0
Test custom toplevel that exits with code 0

```trycmd
$ scryer-prolog -f --no-add-history -t success_toplevel tests/scryer/cli/fixtures/toplevel_test_helper.pl
SUCCESS_TOPLEVEL_EXECUTED

```

## Custom toplevel with file loading
Test that custom toplevel can access predicates from loaded file

```trycmd
$ scryer-prolog -f --no-add-history -t test_file_loaded tests/scryer/cli/fixtures/toplevel_test_helper.pl
LOADED_PREDICATE_CALLED

```

## Custom toplevel with -g goal
Test combining -g goal with custom toplevel

```trycmd
$ scryer-prolog -f --no-add-history -g "helper_predicate" -t halt tests/scryer/cli/fixtures/toplevel_test_helper.pl
Helper predicate works

```

## Multiple goals with custom toplevel
Test multiple -g goals before custom toplevel

```trycmd
$ scryer-prolog -f --no-add-history -g "write('First goal'), nl" -g "write('Second goal'), nl" -t halt tests/scryer/cli/fixtures/toplevel_test_helper.pl
First goal
Second goal

```

## File loading then custom toplevel
Test that files are loaded before toplevel runs

```trycmd
$ scryer-prolog -f --no-add-history -t write_and_exit tests/scryer/cli/fixtures/toplevel_test_helper.pl
Output from custom toplevel

```

## Undefined toplevel predicate
Test error handling when toplevel predicate doesn't exist

```trycmd
$ scryer-prolog -f --no-add-history -t undefined_predicate
? failed
error(existence_error(procedure,undefined_predicate/0),undefined_predicate/0).

```

## Test that default behavior unchanged
Without -t flag, a simple goal should still work (using halt to avoid REPL)

```trycmd
$ scryer-prolog -f --no-add-history -g "write('No custom toplevel'), nl, halt"
No custom toplevel

```

## g_caused_exception/2 with exception thrown
Test that g_caused_exception/2 is asserted when -g goal throws exception

```trycmd
$ scryer-prolog -f --no-add-history -g "throw(test_error)" -t check_exception_halt_1 tests/scryer/cli/fixtures/toplevel_test_helper.pl
? 1
throw(test_error) causes: test_error
EXCEPTION_CAUGHT
Goal: throw(test_error)
Exception: test_error

```

## g_caused_exception/2 with no exception
Test that g_caused_exception/2 is not asserted when -g goal succeeds

```trycmd
$ scryer-prolog -f --no-add-history -g "write('Success')" -t check_exception_halt_0 tests/scryer/cli/fixtures/toplevel_test_helper.pl
SuccessSUCCESS_NO_EXCEPTION

```

## g_caused_exception/2 with error() term
Test that g_caused_exception/2 captures error/2 terms correctly

```trycmd
$ scryer-prolog -f --no-add-history -g "throw(error(type_error(integer, foo), context))" -t check_exception_halt_1 tests/scryer/cli/fixtures/toplevel_test_helper.pl
? 1
throw(error(type_error(integer,foo),context)) causes: error(type_error(integer,foo),context)
EXCEPTION_CAUGHT
Goal: throw(error(type_error(integer,foo),context))
Exception: error(type_error(integer,foo),context)

```