Support higher-order function sorts and partial application - #122
Open
daniel-larraz wants to merge 2 commits into
Open
Support higher-order function sorts and partial application#122daniel-larraz wants to merge 2 commits into
daniel-larraz wants to merge 2 commits into
Conversation
Z3Py models a lambda as an array, so defining a function by a lambda means declaring it with an array range and equating the application with the lambda. cvc5 keeps function and array sorts distinct, and neither half of that pattern could be expressed: Function() raised because cvc5 refuses a function sort as a codomain, and FuncDeclRef.__call__ insisted on a saturating number of arguments. Both are reachable in cvc5 through its native higher-order support, so wire them up. Function() now flattens a function-sort range. cvc5 normalizes higher-order sorts, so `Int -> (-> Real Bool)` is built as `(-> Int Real Bool)`; applying the leading domains yields the range sort again, which keeps the distinction invisible. FreshFunction() shares the same helper. FuncDeclRef.__call__ builds a partial application out of HO_APPLY when given too few arguments, or when the function is a partial application itself, since neither is expressible with APPLY_UF. The result carries the rest of the function sort and is callable again, so `setof(i)` has the lambda's sort and `setof(i) == body` typechecks. The printer gains an HO_APPLY case. It had none, so printing a partial application raised "Cannot print: Kind.HO_APPLY". The curried spine is collapsed, so `f(x)(y)` reads as `f(x, y)` - an equal term, printed the way a saturated application is. This makes the terms constructible, not the problem decidable: cvc5 reasons about function terms only under an HO_ logic, and a quantified definition needs ho-elim to be discharged rather than answered unknown. Both are documented in the docstrings and covered by the new test. Addresses the remaining item in cvc5#100. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Z3Py models a lambda as an array, so it applies one -- and anything defined by one -- with []. cvc5 gives them function sorts, which are applied with (). With only () accepted, no way of writing the use site worked in both: [] was rejected here, () and a saturated call were rejected by Z3Py, leaving the intersection empty even though the whole definition was already portable. Accept [] as a second spelling. FuncDeclRef.__getitem__ applies, taking a tuple for several arguments at once, and QuantifierRef.__getitem__ applies a lambda. Select is left alone: it is an array operation, and a lambda has a function sort here, not an array sort. Nothing is lost by that, because Z3Py defines Select(a, i) as a[i] -- so a select of a lambda rewritten as L[i] goes on working under both. A lambda is applied a term at a time rather than through a FuncDeclRef view of it: the wrapper would claim a type the term does not have, and the printer cannot render a lambda as a declaration -- it raises while building the arity assertion message in _higherorder_apply, which is formatted whether or not the assertion holds. The example that motivated cvc5#100 now runs unmodified under both, once the solver is constructed conditionally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Z3Py models a lambda as an array, so defining a function by a lambda means declaring it with the lambda's sort as the range and equating the application with the lambda. cvc5 keeps function and array sorts distinct, and neither half of that pattern could be expressed:
Declaring the range as an array sort instead does not help:
g(lo, hi) == bodythen fails withsort mismatch, the(Array Real Bool)vs(-> Real Bool)clash from #100.All of it is reachable through cvc5's native higher-order support. The pythonic layer just never wired it up.
Result
This program now runs unchanged under Z3Py and cvc5, and prints the same thing under both — only the solver construction differs:
Changes
Functionflattens a function-sort range (first commit). cvc5 normalizes higher-order sorts, soReal -> (-> Real Bool)is built as(-> Real Real Bool). Applying the leading domains yields the range sort again, so the distinction stays invisible:FreshFunctionshares the same helper.FuncDeclRef.__call__builds partial applications (first commit). Given too few arguments -- or when the function is itself a partial application -- it emits anHO_APPLYchain, since neither case is expressible withAPPLY_UF. The result carries the rest of the function sort and is callable again, sosetof(lo, hi) == bodytypechecks.The printer gains an
HO_APPLYcase (first commit). It had none, so printing a partial application raisedCannot print: Kind.HO_APPLY. The curried spine is collapsed, sof(x)(y)reads asf(x, y)-- an equal term, printed the way a saturated application is. The head of the spine need not be a name (If(c, f, g)(i)is a function-sorted term too), so the group is composed directly rather than throughseq1, which measures its header.[]is accepted as a second spelling of application (second commit). Z3Py applies a lambda, and anything defined by one, with[]; without it there was no way to write the use site that both accept --[]was rejected here,()and a saturated call are rejected by Z3Py, so the intersection was empty even though the whole definition was already portable.Selectis deliberately left alone. It is an array operation, and a lambda has a function sort here, not an array sort. Nothing is lost by that: Z3Py definesSelect(a, i)asa[i]---- so a select of a lambda rewritten as
L[i]goes on working under both. I checked.eq()onSelect(x, ...)againstx[...]in Z3Py for 1-dim and 2-dim arrays, integer indices,Storeresults,K, nested selects, model values, and 1- and 2-argument lambdas: equal in every case.Scope
This makes the terms constructible, not the problem decidable. cvc5 reasons about function terms only under a logic carrying the
HO_prefix, and a quantified definition needsho-elimto be discharged rather than answeredunknown:Both are documented in the docstrings and exercised by the new test. Applying a lambda directly needs neither -- a beta-redex reduces before the solver sees a function term, so
Lambda([x], x + 1)[3]works under a plainSolver().I did not have
Solversniff assertions for function sorts and silently upgrade the logic: overriding a deliberately pinned logic seems worse than an error namingHO_ALL.Where this accepts more than Z3Py
Worth flagging for review, since it is one-directional -- code written against cvc5 using these will not port back:
f[i]andf[i, j]on a plain uninterpreted function. Z3Py raisesTypeError.L2[3]-- partial indexing of a two-argument lambda. Z3Py raisesselect requires 3 arguments.f(i)-- partial application generally, which is the point of the first commit.Keeping
[]uniform across function-sorted terms was a deliberate choice over restricting it to exactly Z3Py's set; the alternative rules all draw awkward lines (rejectingf[i]but allowingg(i)[j], say). Happy to narrow it if you would rather the two agree exactly.Also unchanged:
decl()still raises on a partial application andchildren()still includes the function, both following the existing convention that onlyAPPLY_UFis destructured.Relation to #100
This addresses the remaining item there. The two items reported in that issue were fixed by #118 and #120; this is the function/array interop underneath them.
Testing
test/pgms/higher_order.pycovering the path end to end: the flattened sort, partial application and its printing, a non-name application head, both spellings agreeing viaeq,Selectstill refusing a function, andcheck()in both directions.test_doc.py: 2124 doctests, 0 failures.test_unit.py: OK.black --check --required-version 24: clean.pyright: 635 errors against 633 on main. The two arectx.tm.mkTermon an optional context in the new__getitem__, one more instance each of a pattern the file already has 119 and 93 of.decl()/children(),FreshFunction, datatype constructor/selector/tester application (they subclassFuncDeclRefbut define their own__call__), and the arity errors for too many arguments and forf().🤖 Generated with Claude Code