scryer-prolog icon indicating copy to clipboard operation
scryer-prolog copied to clipboard

predicate_property(M:P) fails silently

Open rotu opened this issue 8 months ago • 11 comments

I think predicate_property is supposed to fail with an instantiation error in both of these cases:

?- predicate_property(P,A).
   error(instantiation_error,predicate_property/2).
?- predicate_property(M:P,A).
   false.

rotu avatar Apr 20 '25 19:04 rotu

Good point, thank you a lot!

I posted an implementation proposal in https://github.com/mthom/scryer-prolog/pull/2902/commits/ee6e34bd6749b7fad845bba3a3c3b93d4d6092ee. However, with this change, Scryer crashes on startup. https://github.com/mthom/scryer-prolog/pull/2902/commits/afc3aa253a631d52161640338553c42ce5a3c547 shows how to disable the branch that throws instantiation errors when only the module is a variable. This must be addressed before merging, since it may lead to wrong results or crashes. I would appreciate all input. Thank you a lot!

triska avatar Apr 21 '25 19:04 triska

No, thank YOU! I'm incredibly lost in Prolog, and I don't even know how to ask the right questions. But at least errors provide some guidance when I'm doing something nonsensical!

This code is quite puzzling. I thought that built-ins are implemented in non-prolog (and don't have a "defining module" in their predicate properties).

Things I think I now understand:

  1. The "real" built-ins are named with atoms starting with $ (dollar sign).
  2. All standard builtins and control constructs are actually implemented in prolog on top of these (even if a trivial wrapper like fail :- '$fail'.)
  3. Most standard builtins are in builtins.pl. Some are in loader.pl. This shouldn't matter much since they are visible everywhere.

I don't know if there is any observable difference between a predicate called in the context of builtins vs loader? The only difference I can see is:

?- clause(loader:length(_,_),_).
   error(permission_error(access,private_procedure,length/2),clause/2).
?- clause(builtins:length(_,_),_).
   false.

rotu avatar Apr 21 '25 21:04 rotu

There are actually a few builtins that are defined in Rust and not in Prolog without a $ intermediary. They currently don't appear in the documentation which is unfortunate, see #2030.

length/2 is actually from library(lists). It is loaded in the context of the module loader because it is imported there with use_module. That means it is also loaded by default in user (the default module), but none of it's predicates are visible. That is, length(A, B) doesn't work on startup, but lists:length(A,B) does work. builtins:length(A,B) doesn't work because library(builtins) doesn't import library(lists) at all, so none these predicates are visible there. As you may have noticed, it's often helpful to think of modules more as contexts than what you'd think of as modules in other languages.

bakaq avatar Apr 21 '25 21:04 bakaq

Regarding terminology:

3.21 built-in predicate: A procedure whose execution
is implemented by the processor (see 8).
8 Built-in predicates

A built-in predicate is a procedure which is provided
automatically by a standard-conforming processor.

triska avatar Apr 21 '25 21:04 triska

There are actually a few builtins that are defined in Rust and not in Prolog without a $ intermediary. They currently don't appear in the documentation which is unfortunate, see #2030.

length/2 is actually from library(lists). It is loaded in the context of the module loader because it is imported there with use_module.

Yep - that's why I chose it as an example.

It seems like an accident that builtins and loader are modules at all. These are the ways it seems to be observable:

  1. going out looking for trouble (as I did) by explicitly qualifying a term
  2. trying to create a module with these names
  3. (when implemented in the future) the output of ?- predicate_property(use_module(_), defined_in(M)).

Regarding terminology

User-defined procedures are organized into modules but built-in procedures aren't and are visible everywhere. It seems somewhat oxymoronic to even have a builtins module since built in predicates can be neither imported nor exported.

rotu avatar Apr 22 '25 00:04 rotu

User-defined procedures are organized into modules

For user-defined procedures, the standard says:

A user-defined procedure is a sequence of (zero or more)
clauses prepared for execution.

triska avatar Apr 22 '25 01:04 triska

For user-defined procedures, the standard says:

A user-defined procedure is a sequence of (zero or more) clauses prepared for execution.

Predicates defined in builtins.pl and loader.pl fit that definition :-p

rotu avatar Apr 22 '25 02:04 rotu

The key point is that modules are not mentioned in any of these definitions.

triska avatar Apr 22 '25 02:04 triska

User defined procedures always belong to a module. Per 4.4.2 The module user:

A Prolog processor shall support a built-in module user. User-defined procedures not defined in any particular module shall belong to the module user.

Based on a few things, I think built-ins can't exist in a module in any meaningful sense. I have to read between the lines a little because no prolog processor implements the idea of a "module interface" and "module body" as different things:

  1. Per 6.3, every procedure that is neither a control construct nor a built-in predicate belongs to some module. (weakly implying that built-in predicates don't need to belong to any module)
  2. Per 6.2.6, The predicate indicator in a clause-term in module text may NOT be a built-in predicate or control construct.
  3. Per 6.2.4., 6.2.5. the built-in predicates may not be specified by the predicate indicators in directives, specifically export/1, reexport/2, reexport/1, import/1, import/2.
  4. Per 6.2.6, the head of a clause in module text can't match any built-in predicate indicator.

It seems un-useful, and arguably wrong, that builtins and loader are modules. I think it's fairly benign (except that builtins shows up in the docs whereas loader does not).

rotu avatar Apr 22 '25 17:04 rotu

User defined procedures always belong to a module. Per 4.4.2 The module user:

A Prolog processor shall support a built-in module user. User-defined procedures not defined in any particular module shall belong to the module user.

With hindsight, I can see that this expectation was rooted in ISO/IEC 13211-2.

I think I should instead read the (:)/2 and predicate_property/2 predicates as an implementation-specific extension. It would be nice to bootstrap :, but I don't even know if that's feasible!

rotu avatar May 15 '25 21:05 rotu

I tried to understand your attempted fix in #2902, but I got stuck on strip_module/3, which is where I think the bug originates.

Its behavior differs between scryer and swipl (output var names changed for legibility):

:- initialization((strip_module(M:t,B,C),writeq(strip_module(M:t,B,C)),nl)).

In scryer, this prints strip_module(M:t,M,t) In swipl, this prints strip_module(M:t,user,M:t)

When even less instantiated, scryer gives a concrete module again:

:- initialization((strip_module(A,B,C),writeq(strip_module(A,B,C)),nl)).

prints strip_module(A,user,A) in both scryer and swipl.

rotu avatar May 16 '25 05:05 rotu