:- module(tests, [run_tests/0]). :- use_module(parser). :- use_module(types). :- use_module(log). run_test(TestName, Env, Code, ExpectedTypeResult) :- format('--- Test: ~w ---~n', [TestName]), ( parse(Code, AST) -> format('Parsed AST: ~w~n', [AST]), log(test_setup, env(Env)), ( catch(infer_type(AST, Env, ActualType), Error, ( log(error, caught_error(Error)), explain_error(Error, Explanation), format('Type Error: ~w~n', [Explanation]), ActualType = error(Error) % Represent error for comparison )) -> true ; ActualType = 'inference_failed_silently' % Should not happen if catch works ), format('Inferred Type: ~w~n', [ActualType]), ( (ActualType == never, ExpectedTypeResult \== never) -> % Explicitly fail if 'never' is inferred unexpectedly Pass = false, SubMatch = 'unexpected_never' ; (ExpectedTypeResult = error(_), ActualType = error(_)) -> % Both are errors Pass = true, SubMatch = 'error_expected_and_received' ; ExpectedTypeResult == ActualType -> Pass = true, SubMatch = 'exact_match' ; unify_types(ExpectedTypeResult, ActualType, ExpectedTypeResult) -> % Actual is subtype of Expected Pass = true, SubMatch = 'subtype_match' ; unify_types(ExpectedTypeResult, ActualType, ActualType) -> % Expected is subtype of Actual (and Actual is not 'never' unless Expected is also 'never') Pass = true, SubMatch = 'supertype_match' ; Pass = false, SubMatch = 'mismatch' ), ( Pass == true -> format('Status: PASS (~w)~n~n', [SubMatch]) ; format('Status: FAIL (~w) - Expected: ~w~n~n', [SubMatch, ExpectedTypeResult]) ) ; format('Parse FAILED for code: ~s~n~n', [Code]) ). run_tests :- set_verbosity(1), % Set verbosity: 0 (errors), 1 (log), 2 (debug) log(tests, 'Starting test suite...'), initial_env(EmptyEnv), run_test('Conditional with is_number/1 (x is number)', [x:union(number,string)], "(if (is_number x) x 0)", % x is union(number,string), then branch x is number. else branch x is string. % 0 is number. So then branch is number, else branch is number. % Result should be number. number), % If x is number, then x (number). Else 0 (number). Unified: number. run_test('Conditional with is_number/1 (x is string)', [x:union(number,string)], "(if (is_number x) 1 \"not num\")", % x:union(number,string). % Cond: (is_number x) % Then: x refined to number. Body `1` is number. % Else: x refined to string. Body `"not num"` is string. % Result: union(number, string) union(number, string)), run_test('Pattern match list', [my_list:list(number)], "(match my_list (((list a b) a)))", % my_list:list(number). a,b become number. returns a (number). % Pattern (list a b), body a number), run_test('Pattern match tuple', [my_tuple:tuple([number, string])], "(match my_tuple (((tuple x y) y)))", % my_tuple:tuple([number, string]). x is number, y is string. returns y (string). % Pattern (tuple x y), body y string), run_test('Let binding', % z:number, trying to assign string. Current 'let' rebinds. [z:number], "(let z \"text\" z)", string), % With current 'let' semantics (rebinding), z will be string. Env [z:number] is shadowed. run_test('Unification in conditional branches', EmptyEnv, "(if true 10 \"text\")", % ThenType=number, ElseType=string. unify_types(number,string) -> union(number,string) union(number,string) ), run_test('Successful refinement (simulated validate_user)', [user_data:any], % Using is_number to simulate a predicate that refines type. % Env: [user_data:any] % (if (is_number user_data) user_data "not a number") % Cond: (is_number user_data) -> boolean % Then branch: user_data refined to number. Body: user_data -> number % Else branch: user_data refined to not(number). Body: "not a number" -> string % Result: union(number, string) "(if (is_number user_data) user_data \"not a number\")", union(number, string) ), run_test('Lambda expression (syntax check, type not deeply inferred yet)', EmptyEnv, "(lambda (x y) (add x y))", % `add` is not a defined function, so type of body is an issue. % For now, this tests parsing of lambda. % Expected type depends on how `add` and lambdas are typed. % Let's expect 'any' or a placeholder function type if types.pl is not updated for lambdas. % For now, let's assume it's 'any' as `add` is unknown. any), % Placeholder: Actual type depends on full function type inference. run_test('Function application of lambda (syntax check)', EmptyEnv, "((lambda (x) x) 10)", any), % Placeholder: Actual type depends on lambda type inference and application rules. % If lambda is (T->T) and arg is T, result is T. Here, (any->any) and number -> any. % If (lambda (x) x) is typed as fun_type([any],any), then apply to int(10) (number) -> any. run_test('Empty list literal', EmptyEnv, "()", list(never) % Or some polymorphic list type list(T) if supported. list(never) is common for empty. ), run_test('Boolean true literal', EmptyEnv, "true", boolean ), run_test('Boolean false literal', EmptyEnv, "false", boolean ), % --- Function Type Tests --- run_test('Lambda returning number', EmptyEnv, "(lambda (x) 10)", % x is any, body is 10 (number) fun_type([any], number) ), run_test('Lambda identity function', EmptyEnv, "(lambda (x) x)", % x is any, body is x (any) fun_type([any], any) ), run_test('Lambda with two params, returning one', EmptyEnv, "(lambda (x y) y)", % x,y are any, body is y (any) fun_type([any, any], any) ), run_test('Lambda with no params, returning string', EmptyEnv, "(lambda () \"hello\")", fun_type([], string) ), run_test('Apply identity lambda to number', EmptyEnv, "((lambda (x) x) 10)", % (lambda (x) x) -> fun_type([any],any). Applied to 10 (number). Result: any. any ), run_test('Apply lambda returning number', EmptyEnv, "((lambda (x) 10) \"text\")", % (lambda (x) 10) -> fun_type([any],number). Applied to "text" (string). Result: number. number ), run_test('Apply lambda with no params', EmptyEnv, "((lambda () \"world\"))", % fun_type([], string) applied to (). Result: string. string ), run_test('Apply lambda with arity mismatch (too few args)', EmptyEnv, "((lambda (x y) x) 10)", % fun_type([any,any],any) applied to (10). Arity mismatch. never % Or error specific representation ), run_test('Apply lambda with arity mismatch (too many args)', EmptyEnv, "((lambda (x) x) 10 20)", % fun_type([any],any) applied to (10, 20). Arity mismatch. never % Or error specific representation ), run_test('Apply non-function (number)', EmptyEnv, "(10 20)", % AST: apply(int(10), [int(20)]) never % Or error specific representation ), run_test('Apply variable holding a function', [f:fun_type([number], string)], "(f 123)", % f is fun_type([number],string). Applied to 123 (number). Result: string. string % This test will require unification of number with number for param. ), run_test('Apply variable holding a function - arg type mismatch (placeholder)', [id_fun:fun_type([number], number)], % Expects number "(id_fun \"text\")", % Called with string. Current 'any' in lambda def won't catch this. % If id_fun was defined as (lambda (x::number) x), this would be 'never'. % For now, if ExpectedParamType is 'number' and Actual is 'string', % compatible_arg_types should fail if unify_types(number, string, union(...)) % and we check UnifiedType \= never. % Let's assume unify_types(number, string, union(number,string)) % union(number,string) \= never, so this would pass if we don't check for subtype. % Let's refine compatible_arg_types to be stricter or unify_types to return never for incompatible base types. % For now, let's assume the current unify_types(T1,T2,union(T1,T2)) for disparate types. % And compatible_arg_types checks `UnifiedType \= never`. % This test will pass with `number` if `id_fun` is `fun_type([any],number)`. % If `id_fun` is `fun_type([number],number)`, then `unify_types(number, string, union(number,string))` % `union(number,string) \= never` is true. % The rule `unify_types(T1, T2, union(T1, T2))` needs to be before any catch-all. % The `compatible_arg_types` should ideally be `unify_types(Expected, Actual, Expected)` % or `unify_types(Expected, Actual, Unified)` and `is_subtype(Unified, Expected)`. % Let's adjust `compatible_arg_types` for stricter checking. never % Expecting failure due to type mismatch. ), log(tests, 'Test suite finished.'). % To run: % ?- consult('log.pl'). % ?- consult('parser.pl'). % ?- consult('types.pl'). % ?- consult('tests.pl'). % ?- run_tests.