diff --git a/src/toplevel.pl b/src/toplevel.pl index 0727aee03..ce0190c73 100644 --- a/src/toplevel.pl +++ b/src/toplevel.pl @@ -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) -> @@ -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) :- @@ -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 ), @@ -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'), nl, write(' -f '), write('Fast startup. Do not load initialization file (~/.scryerrc)'), nl, write(' --no-add-history '), @@ -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'). @@ -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: '), diff --git a/tests/scryer/cli/fixtures/toplevel_test_helper.pl b/tests/scryer/cli/fixtures/toplevel_test_helper.pl new file mode 100644 index 000000000..3ded55dca --- /dev/null +++ b/tests/scryer/cli/fixtures/toplevel_test_helper.pl @@ -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) + ). diff --git a/tests/scryer/cli/src_tests/custom_toplevel.md b/tests/scryer/cli/src_tests/custom_toplevel.md new file mode 100644 index 000000000..69786ace4 --- /dev/null +++ b/tests/scryer/cli/src_tests/custom_toplevel.md @@ -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) + +```