didoudiaz / gprolog

GNU Prolog
Other
106 stars 13 forks source link

Built-in predicate request: function_property/2 (renamed as evaluable_property/2) #40

Closed pmoura closed 1 year ago

pmoura commented 1 year ago

Latest versions of LVM and Trealla Prolog implement a function_property/2 built-in predicate, which provides similar functionality to the de facto standard predicate_property/2 built-in predicate, allowing checking or enumerating the properties of a given arithmetic function. The function_property/2 predicate allows clean checking if an arithmetic function is defined, simplifying e.g. portability linter checks for arithmetic expressions. The first argument is a function template (e.g. abs(_)) and the second argument is the property. Four properties are specified:

The dynamic is meant for Prolog systems that allow runtime definition of new arithmetic functions (e.g. LVM). Built-in and foreign functions usually also have the static property.

The predicate spec is:

Template: function_property(Function, Property) Modes and number of proofs: function_property(+callable, ?function_property) - zero_or_more

Exceptions:

Examples:

Check that popcount/1 is a built-in arithmetic function:

?- function_property(popcount(_), built_in).
true.

Would it be possible to add this predicate to GNU Prolog?

pmoura commented 1 year ago

Update

An additional property, that enables e.g. more sophisticated linter checks, is template/2. It allows querying function arguments types and return types. For example:

?- function_property(abs(_), Property).
   Property = built_in ;
   Property = static ;
   Property = template(abs(integer), integer) ;
   Property = template(abs(float), float) ;
   false.

For some Prolog systems, the necessary internal tables to implement this property seems to already be there. For other, it would be more work. And in the case of GNU Prolog?

didoudiaz commented 1 year ago

What would be the templates of (+)/2 ?

pmoura commented 1 year ago
template(integer + integer, integer)
template(float + float, float)
template(float + integer, float)
template(integer + float, float)
pmoura commented 1 year ago

A more concise (but also ambiguous) alternative would be:

template(number + number, number)
didoudiaz commented 1 year ago

I'm not sure this property is easy to implement in gprolog.

About your last proposal: it seems close to the signature used in gprolog math doc.

pmoura commented 1 year ago

Yes, template/2 provides access to function signatures. I understand this property may be difficult to implement depending on the Prolog system internals. But, if most systems implement this predicate with at least the first four properties, it will already be nice progress (specially compared with the verbose and error prone alternative of resorting to catch/3 calls to check if a function is defined).

didoudiaz commented 1 year ago

As you mentioned in #41, the ISO termonology is evaluable functor. For instance there is an type_error(evaluable,...). I think the most adequate name for the new built-in predicate should be evaluable_property/2 instead of function_property/2?

pmoura commented 1 year ago

Two systems (Trealla Prolog and LVM) already implement function_property/2 and others systems are committed to implement it soon using this name. Most users think in terms of predicates and arithmetic functions. Isn't that fine distinction between evaluable functor and function non-consequential? The only cases where it might be a bit odd talking about functions is the arithmetic constants (e.g. pi).

pmoura commented 1 year ago

(talking with Andrew about your name suggestion)

didoudiaz commented 1 year ago

Nice ! It is easy to change the name now (simpler than later if we discover one day it was not the best choice). I believe, it better correspond to Prolog terminology and avoids the confusion functor/function (from user's point of view).

pmoura commented 1 year ago

Both me and Andrew are fine with the name change.

didoudiaz commented 1 year ago

Great. BTW: I saw the trealla version requires the evaluable is provided. On the other hand, for consistency with predicate_property/2, gprolog evaluable_property/2 is (will be) reexecutable on backtracking to discover all evaluables.

pmoura commented 1 year ago

Note that predicate_property/2 requires a bound first argument per spec. The now named evaluable_property/2 does the same.

pmoura commented 1 year ago

Allow enumeration by backtracking is, however, more troublesome for predicate_property/2 than evaluable_property/2 with several systems providing call/N as a built-in predicate with large values for N . Enumeration may also expose predicates/evaluables not officially documented. That said, I do see the value of being able to enumerate both. But I don't think these should be the predicates doing the enumeration.

didoudiaz commented 1 year ago

I didn't notice predicate_property/2requires a ground prototype. Note that, for predicates, current_predicate/1 is backtrackable, and thus presents the same issues you mention. In gprolog, system predicates (beginning with a $) are not returned. I think it is nice to discover all available evaluables. So, why not a current_evaluable/1?

pmoura commented 1 year ago

Part of the problem here is that the specification of predicate_property/2 belongs in the Core standard but it's only found in the doomed Modules standard. Not that I didn't do my best to fix this while WG17 Core editor: at the 2009 WG17 meeting in Pasadena it was unanimously approved to move the spec to the Core (minus, of course, the module only properties).

pmoura commented 1 year ago

A current_evaluable/1 predicate makes sense as a counterpart to current_predicate/1. But the later is specified to only enumerate user-defined predicates (a detail that several systems ignore, allowing it to enumerate, via backtracking, all defined predicates, system and user-defined). That said, if enumeration is to become the de facto standard for both, personally, I would prefer it only in the current_.../1 predicates.

pmoura commented 1 year ago

The latest Logtalk git version provides a test set for the evaluable_property/2 predicate:

https://github.com/LogtalkDotOrg/logtalk3/tree/master/tests/prolog/predicates/evaluable_property_2

The test only run when the predicate is defined. Latest LVM and Trealla Prolog pass all tests.

didoudiaz commented 1 year ago

For consistency with stream_property/2, predicate_property/2(and atom_property/2 and file_property/2 in gprolog), evaluable_property/2 should not raise a type_error on Property but a domain_error (this allows for future extenion with new properties of any type).

Errors should be:

pmoura commented 1 year ago

Can you think of a single reasonable example where an evaluate, predicate, stream, or atom property would not be a callable term? I can’t. The alternative would be for a property to be a number. But such number would be meaningless without a wrapper. The issue where is not the type error in the current tests for the evaluable_property/2 predicate but that this type error is missing in all the other property predicates.

didoudiaz commented 1 year ago

I agree a type_error makes sense. However, I would be in favour of consistency (as a user, I always find it confusing when this isn't satisfied). All xxx_property predicates should report similarly errors. Obviously, the key point is that the error is reported and the reported error is not abherant. A domain_error is not abherant.

pmoura commented 1 year ago

Agree that consistency is important. But consistency in error handling also dictates that domain errors are only issued when there isn't a type error. I looked into property type-checking for implementations of the predicate_property/2 and stream_property/2 predicates among several systems. Some divergence in behavior, to no one surprise. For now at least, and for the evaluable_property/2 predicate tests, I changed them so that both type and domain errors are accepted when the property is neither a variable or a callable term. Key point, with either exception, a programming error is detected instead of a misleading failure.

didoudiaz commented 1 year ago

The last commit contains the implementation of current_evaluable/1 and evaluable_property/2. I have added a property iso which I think is interesting (if it is an ISO evaluable functor). This version accepts a variable as first argument (I will see if I keep this later). For the template, I have adopted your proposal: e.g. template(number + number, number). ISO uses the template number + number = number (see Core 1-9.3.1.2) . Not sure what is better.

In any case, the above template means: if one argument is a float, the result is a float. I have one case of ambiguity: for min/2(and max/2)) wheretemplate(min(number,number),number)means instead _the returned value the same as the selected argument_ which can be an integer even if the other is a float (as inmin(2,4.5)`. Maybe we can collect all ambiguous case and decide later.

pmoura commented 1 year ago

With the current tests:

$ logtalk_tester -p gnu
% Batch testing started @ 2023-06-24 22:51:47
%         Logtalk version: 3.67.0-b02
%         GNU Prolog version: 1.6.0
%
% logtalk/tests/prolog/predicates/evaluable_property_2
%         16 tests: 0 skipped, 15 passed, 1 failed (0 flaky)
%         completed tests from object tests in 2 seconds
%         clause coverage n/a
%
% Compilation errors/warnings and failed unit tests
% (compilation errors/warnings might be expected depending on the test)
!     commons_evaluable_property_2_01: failure (in 0.0 seconds)
!       test goal succeeded but should have thrown an error:
!         expected error(instantiation_error,A)
!       in file logtalk/tests/prolog/predicates/evaluable_property_2/tests.lgt between lines 32-33
%
% Failed tests
logtalk/tests/prolog/predicates/evaluable_property_2/tests.lgt - commons_evaluable_property_2_01 @ tests
%
% 1 test sets: 1 completed, 0 skipped, 0 broken, 0 timedout, 0 crashed
% 16 tests: 0 skipped, 15 passed, 1 failed (0 flaky)
%
% Batch testing ended @ 2023-06-24 22:51:51
didoudiaz commented 1 year ago

The error is expected since a variable is accepted. This allow for reexecutions by backtracking making it possible to discover all available evaluable functors. For instance, collect all iso evaluables: findall(X, evaluable_property(X,iso),L).

pmoura commented 1 year ago

The same could be done by calling current_evaluable/1 before calling evaluable_property/2. Not clear to me the best option here. As I already mentioned, predicate_property/2 requires a bound first argument.

P.S. The iso property may be a good idea. Still, the are evaluable functors implemented by most systems (e.g. the inverse hyperbolic functions) there are not yet in an official ISO document.

didoudiaz commented 1 year ago

I agree (even if it is a bit more complicated since current_evaluable/1 handles an F/N term (i.e. a predicate indicator) while evaluable_property/2 a prototype.

The key point is to be able to query the system (introspection). Note that it is not possible with current_predicate/1 and predicate_property/2 limited to a ground prototype since built-ins are not returned by current_predicate/1. Thus there is no way to discover all available built-ins. On the other hand, accepting a variable it is possible: findall(X, predicate_property(X,built_in),L).

pmoura commented 1 year ago

I know of several sensible use cases for checking if a predicate or an evaluable functor is built-in (e.g. when writing portability tests or meta-interpreters). But never actually found a use case where it was required to enumerate all built-ins.