I was hoping to avoid having to note about complex terms because the details get a lot more complicated. While I have used what you suggested, a current technique that I am using for my PostScript interpreter which carries around a lot of state in an environment variable is this:
environment/1
returns the initial environment.
environment(
environment(
stacks(
operand_stack([]),
Dictionary_stack,
execution_stack([]),
graphics_state_stack([]),
clipping_path_stack([])
),
Save_level
)
) :-
dictionary_stack(Dictionary_stack),
intial_save_level(Save_level).
dictionary_stack(Dictionary_stack) :-
dictionary_user(Dictionary_user),
dictionary_global(Dictionary_global),
dictionary_system(Dictionary_system),
Dictionary_stack =
dictionary_stack(
[
Dictionary_user,
Dictionary_global,
Dictionary_system
]
).
dictionary_user(Dictionary_user) :-
Dictionary_user = dict(user,[]).
dictionary_global(Dictionary_global) :-
Dictionary_global = dict(global,[]).
dictionary_system(Dictionary_system) :-
Dictionary_system =
dict(system,
[
kv('[',operator('[')),
kv(']',operator(']')),
kv('<<',operator('<<')),
kv('>>',operator('>>')),
kv(=,operator(=)),
kv(add,operator(2,add)),
kv(clear,operator(clear)),
kv(cleartomark,operator(cleartomark)),
kv(copy,operator(copy)),
kv(count,operator(count)),
kv(counttomark,operator(counttomark)),
kv(cvi,operator(cvi)),
kv(def,operator(def)),
kv(div,operator(2,div)),
kv(dup,operator(dup)),
kv(exch,operator(exch)),
kv(false,literal(boolean(false))),
kv(idiv,operator(2,idiv)),
kv(index,operator(index)),
kv(mod,operator(2,mod)),
kv(mul,operator(2,mul)),
kv(null,literal(null)),
kv(pop,operator(pop)),
kv(roll,operator(roll)),
kv(sub,operator(2,sub)),
kv(true,literal(boolean(true))),
kv(type,operator(type))
]
).
intial_save_level(save_level(0)).
Then there are various PostScript operators that modify the environment, e.g.
executable(Environment0,Environment,operator(dup)) :-
Environment0 = environment(stacks(operand_stack(Operands0),Dictionary_stack,Execution_stack,Graphics_state_stack,Clipping_path_stack),save_level(Save_level)),
dup(Operands0,Operands),
Environment = environment(stacks(operand_stack(Operands),Dictionary_stack,Execution_stack,Graphics_state_stack,Clipping_path_stack),save_level(Save_level)).
dup([],error(stackunderflow)).
dup([Value|Operands0],Operands) :-
Operands1 = [Value,Value|Operands0],
length(Operands1,Length),
'MaxOpStack'(Maxium_operand_stack_size),
(
Length =< Maxium_operand_stack_size
->
Operands = Operands1
;
Operands = [error(stackoverflow),Value|Operands0]
).
Now in the test case to avoid having to check all parts of the environment it just pulls out the parts that are expected to change and uses assertion/1 on them, e.g.
% Test operator dup
postscript_operand_stack_test_case("3 dup" ,operand_stack([integer(3),integer(3)]) ).
postscript_operand_stack_test_case("dup" ,operand_stack(error(stackunderflow)) ).
% dup stackoverflow % See test case 002
test(0001, [forall(postscript_operand_stack_test_case(Input,Expected_operand_stack))]) :-
job(Input,Environment),
dictionary_stack(Dictionary_stack),
Environment = environment(stacks(Operand_stack,Dictionary_stack,execution_stack([]),graphics_state_stack([]),clipping_path_stack([])),save_level(0)),
assertion( Operand_stack == Expected_operand_stack ).
% These test resize the stack to force a stackoverflow error
postscript_operator_with_stack_resize_test_case(3,"1 2 3 dup" ,operand_stack([error(stackoverflow),integer(3),integer(2),integer(1)]) ).
test(0002, [forall(postscript_operator_with_stack_resize_test_case(Stack_size,Input,Expected_operand_stack))]) :-
change_parameter('MaxOpStack',Stack_size,Original_operand_stack_size),
job(Input,Environment),
dictionary_stack(Dictionary_stack),
Environment = environment(stacks(Operand_stack,Dictionary_stack,execution_stack([]),graphics_state_stack([]),clipping_path_stack([])),save_level(0)),
assertion( Operand_stack == Expected_operand_stack ),
change_parameter('MaxOpStack',Original_operand_stack_size,_).
There are other test that check other parts of the environment or combinations of changes but it does keep the length of each test case smaller.
It took some time to write the initial test case and get it so that I could build the data in environment in pieces and use it as needed, but now that the foundation is in place, adding and testing new operators is running like a factory production. However I still have to add some more parts to environment and some of them are very large, but this pattern looks like it will handle it without modification to the test cases.