Repository navigation
Symbolic evaluator overhaul - #27
Merged
Merged
Conversation
…lly for its lambda. This removes the need for effects from the data type. Working on translation from GHC Core. Afterwards will do the big-step semantic for it! Still a lot of open questions remain about how to do eval symbolic eval with this one!
…the 'dataConTagZ'
…' is something you pretty much never want to do...). Adjusted files with type applications to remove the ambiguity this introduced. Also added 'effectful' package
…re direct style sharing using 'IORef'. This makes all the 'Defer' code unecessary and is much cleaner in general really. The main reason for doing it is to allow pruning to happen. We also adjusted the evaluator by treating types and coercions more faithfully to a real evaluator: we disregard types and coercions are zero sized values. This is a lot easier and should also help with evaluation speed!
…y on the pattern match.
…umCon' and 'DataCon' construction.
…ad of requiring all the helpers.
…from the base package, not necessarily from the prelude.
…e function into the shape required for the call.
…sed them to implement a larger number of primitives. Added support for literal values (TODO: case split on 'LitAlt'). Added a global 'Env' which contains embeddings and used unfoldings (to simulate a CAF). Added 'Symbolic' typeclass to builtins (TODO: enable primitive support for the new operation).
…d issue where the 'symbolicP' function was not marked as OPAQUE. Small comment adjustment on 'lookupClass'.
…o implement 'symbolicP' primitive. Added support for some bitvector primitives. Added support for if-then-else primitive. WIP: Adjustments to pretty printer. We have a dedicated collection of thunks, which we can use to purely print the relevant expressions. This way, we also do not duplicate prints by recursing directly (i.e. no duplicate prints of any thunks, which *does not scale at all*).
… need type information that we don't really track elsewhere. It is slightly hacky (as we need to special case application) otherwise it is nice as we create an evidence object that we can actually use. * Fixed issue where the if-then-else on alternatives was too strict on its accumulator (we used foldl'), causing infinite loops. Adjusted to using foldr. * Fixed an issue where the 'force' function would not write the merged result of forcing an 'Ite' 'Thunk'.
…ction (we were missing the '1 <= n' coercion slot). * Force the scrutinee using a bang pattern in 'litAlts' instead of a case expression (is slightly cleaner).
…thunks were created for variables that were actually already forced. * Adjusted primitive operation resolution. Instead of tracking a separate 'PrimOps' mapping, we now extend the global environment with them (we added the same for 'DataCon' now actually, but this is on the fly).
…e it should be a data constructor wrapping the coercion. * Fixed a bug we introduced by removing a separate 'PrimOps' mapping, where arity 0 operations were left unevaluated as the arity check only reduces when adding a value. Now we add constants to the global map for arity 0 operations. In the future, we might want to remove arity 0 operations from 'PrimOp' so we don't have to call the evaluator when adding these to the 'Global' environment. This way, we get rid of some effects that will never be called.
…he Sym monad. * Adjusted symbolic variable generation to do a lazy IO-like identifier generation through the 'Gen' monad.
…ser can for most data types derive the implementation through 'Generically' (or use the default signature). * Added primitives for 'unreachable' and 'undefinedBehaviour', the former is used for vacuous types in the 'GHC.Generics' implementation.
…nctor in 'CollectThunks' instead of defining custom functor PairF.
…ing an error throw in its inner body (even though it had 'noinline'). Now, I've set it to diverge through a self-recursive call with 'noinline', which seems to not trigger the bug. Not sure why it also didn't end up happening everywhere...
…olve type families that get a reduction due to the unification. E.g. if we instantiate '1 <= n' to '1 <= 4', then we can reduce the type family '<=' such that we can satisfy the constraint.
…hing an unfolding into the global environment. With this fixed, the runtime prints are a lot nicer as we do not pull in arbitrary environments that are unused!
…imply is an identity function.
…ir environments: this is space waste as they are already contained in the global environment!
…sed 'KnownNat' conversion back to the Haskell one.
… recursive calls blow through memory. Already added an issue to implement this in a better way. For now, we accept bad call stacks... * Scrutinee thunk in literal cases now are attempted to be resolve as a variable before creating a closure. This is similar to a patch we made for arguments of applications and it really matters a lot for both constraint size and evaluation time. * Also added some exports to WHNF. Not sure if we want to keep those though...
… put the 'NoStrategy' on the 'Tsil' instead of inner value of the 'Union'. The improvement this has on the reduction is massive! * Fixed issue where the 'ThunkKind' 'Ite' constructor would pack its values. This caused their 'StableName' to be different every time it was packed/unpacked. For now, we explicitly annotate that they should not be packed in the data type. We can see in the future if there is a way to have them packed.
… cannot really print it as is. For now, we simply do this to minimize the regression in this aspect.
* Removed the old solver call from 'Pantomime.Solve'. * Removed the old evaluator itself, this accounts for all file removals in here. * Removed some now obsolete code from 'Pantomime.Symbolise'. * Moved the one function we still used from 'Pantomime.Grisette.Mergeable' to 'Pantomime.Util'.
This was referenced Oct 16, 2026
Closed
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.
This is a big adjustment, where we completely rebuilt the symbolic evaluator.
Symbolic evaluator
At a high level, the evaluator is now much closer to the actual evaluator scheme of Haskell. Allocated thunks are managed through
IORef. This way we can apply the evaluation on such aThunkwhile applying a side-effect. Previously we would keep arguments as actualHaskellthunks. This got very hairy... In any case, this closes #9.With us now being able to inspect our runtime better through a
Thunk, we can now print the (reachable) allocations from any expression. This allows us to inspect the RTS at any point of evaluation!Instead of keeping an adjusted form of
Expr(as we did previously), we define aWHNFvalue that is symbolic. This one contains closures, primitives, data constructors and literals. The merging of these is straightforward and does not propagate into its arguments. We also chose to deal with types and coercions in the same wayHaskelldoes: types can be elided and coercions are unit values.A
Thunkcomes in three shapes:WHNFvalue (it was already evaluated)Thunkcontaining the environment and expression to evaluateIte, i.e. a branch of twoThunks. This last one is used when merging e.g. aDataConWHNF.We leave an
Itethunk as non-merged to accommodate for when we want to add in pruning #7: this way we can decide to drop aThunkbefore we actually evaluate it.We also improved branch operations. On a case-of, we now first accumulate all of the patterns that can only match the default case. This way, we can run the default right-hand side once using this accumulate value!
Fresh variable generation
We also did some initial work for the new API as proposed by #6. This one is not fully done however, so we keep it open. The main change is that we now have a
Symmonad in which symbolic statements are run. For now, we only supportfresh, which constructs a fresh value. As we haven't implemented checking primitives, the expected type for checking a proposition isSym Bool.We do close #3 as we created a fresh variable generation API. It works through the
Genmonad. It is a little specific so I'll explain it here. Fresh variable generation should be lazy. For example, we should be able to generate a infinite list of fresh values in a list (without looping forever).There are two ways to achieve this:
Lazy IO gets a very bad name in Haskell (for good reason), but in this case it is very much justified! The side effect we provide is about the same as an oracle that assigns a random unique identifier (which a user cannot actually inspect). Hence, this side-effect is completely harmless.
We use the exact same mechanism as
unsafeInterleaveIO, which is used to implement lazy IO. It works by returning aSymWorldtoken on which the next computation depends. The token is not forced, but lifting of the inner value is blocked by marking the functionNOINLINE(thereby no sharing happens, so we ensure that we do not get two fresh variables for the same call).In the end, the primitive call
symbolic#is of typePrimitive a => Gen a, whereGen :: SymWorld -> a -> Gen a. Notice how we don't need to return aSymWorldas we never need non-lazy values inGen. The monadic bind makes sure that we get the lazy behaviour by callingunsafeDefer(unsafeInterleaveIOforSymWorld) on every bind.The
Genmonad can be lifted into theSymmonad (but not vice versa). Note that we use separate monads because indeedSymshould not have these lazy semantics.