{"id":16338993,"url":"https://github.com/phadej/cabal-solver-sat","last_synced_at":"2026-03-06T19:12:29.300Z","repository":{"id":192393129,"uuid":"686629166","full_name":"phadej/cabal-solver-sat","owner":"phadej","description":"cabal-install solver using sat-simple","archived":false,"fork":false,"pushed_at":"2024-05-05T09:01:23.000Z","size":109,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-03T09:24:02.246Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/phadej.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-09-03T12:35:37.000Z","updated_at":"2024-05-05T09:01:27.000Z","dependencies_parsed_at":"2023-09-04T17:10:54.182Z","dependency_job_id":"9f0bf985-03e8-48f9-81fc-053a1811019f","html_url":"https://github.com/phadej/cabal-solver-sat","commit_stats":null,"previous_names":["phadej/cabal-solver-sat"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/phadej/cabal-solver-sat","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phadej%2Fcabal-solver-sat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phadej%2Fcabal-solver-sat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phadej%2Fcabal-solver-sat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phadej%2Fcabal-solver-sat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/phadej","download_url":"https://codeload.github.com/phadej/cabal-solver-sat/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phadej%2Fcabal-solver-sat/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30192619,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-06T19:07:06.838Z","status":"ssl_error","status_checked_at":"2026-03-06T18:57:34.882Z","response_time":250,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-10T23:53:16.234Z","updated_at":"2026-03-06T19:12:29.282Z","avatar_url":"https://github.com/phadej.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cabal-solver-sat\n\nRecently I wrote [a blog post](https://oleg.fi/gists/posts/2023-08-30-using-cabal-install-solver-as-sat-solver.html) about using `cabal-install` solver as SAT solver.\nNatural question, which I had in the back of my mind for quite long time, is whether we can use off-the-shelf SAT solver as `cabal-install` dependency solver?\nI always assumed that we can, but I never bothered to try out myself.\nSo here we go.\n\nEncoding\n--------\n\nWe need to encode the dependency resolution problem as SAT problem.\nTo encode any problem you need to understand the problem first.\nDependency resolution problem is not that complicated, at least if we simplify it a bit:\n\n1. Input: Collection of packages: which have name, version and dependencies.\n2. Output: A set of package-versions.\n3. Constraint: The resulting set must be coherent: each package in the set has its dependencies also in the set.\n4. Constraint: Requested (targets) must also be in the set.\n\nAn example of package is\n\n```cabal\nname: my-package\nversion: 1.2\n\nlibrary\n  build-depends: base \u003e=4.16 \u0026\u0026 \u003c4.19, containers ^\u003e=0.6.7\n```\n\nObviously `.cabal` files have more information, but to the first approaximation the above is all we need to resolve dependencies.\n\nOne wisdom of SAT encoding is apparently \"try the most obvious encoding first\", and that is what I did.\n\nThe idea is to encode each package-version as own literal. Then dependencies can be encoded as implication clauses (recall: `x → y` is `¬x ∨ y`).\nLet's assume there are `base-4.16.0.0`, `base-4.17.0.0`, etc packages, then the package description above will be encoded as\n\n```text\n¬ my-package-1.2 ∨ base-4.16.0.0 ∨ base-4.16.1.0 ∨ base-4.16.2.0 ∨ ... ∨ base-4.18.0.0\n¬ my-package-1.2 ∨ containers-0.6.7\n```\n\nIf our goal is to build `my-package`, then `my-package-1.2` will be a single literal clause.\n\nAn important bit, somewhat specific to Haskell, is that we must require that *at most one package version per package* is selected.\nSo we need to add a constraint like `assertAtMostOne (base-3.0.3.1, base-3.0.3.2, ..., 4.18.0.0)` which can be encoded in linear amount of clauses.\n\nIf we could select multiple package versions, the dependency resolution problem would become [trivial](https://en.wikipedia.org/wiki/Horn-satisfiability). Intuitively: we could simply select all packages (or greedily select a version when required by `build-depends`).\nThe at most one restriction is the constraint which makes the problem hard.\n\nAnother Cabal specific aspect are *automatic* flags, e.g. \n\n```cabal\nname:    tar\nversion: 0.5.1.1\n\nflag old-bytestring\n  default: False\n  manual: False\n\nlibrary\n  ...\n\n  if flag(old-bytestring)\n    build-depends: bytestring-builder \u003e= 0.10.4.0.2 \u0026\u0026 \u003c 0.11, bytestring == 0.9.*\n  else\n    build-depends: bytestring \u003e= 0.10 \u0026\u0026 \u003c 0.12\n```\n\nWe will encode each flag selection as own SAT variable. I used fresh variables for each package-version (i.e. `tar-0.5.1.1-flag-old-bytestring`),\ninstead of sharing variables across package (i.e. just `tar--flag-old-bytestring`), which is also possible as at most one version is selected.\nLatter would result in less variables, and probably work well, as flag selection is usually consistent across different package versions:\nif `old-bytestring` was off in `tar-0.5.1.0` it probably have to be off in `tar-0.5.1.1` too.\nBut the former option was the one simpler to implement, so I stick with it.\n\nThen the conditional dependencies can be encoded as\n\n```text\n(tar-0.5.1.1 ∧ tar-0.5.1.1-flag-old-bytestring) → (bytestring-builder-0.10.4.0.2 ∨ ... ∨ bytestring-builder-0.10.8.2.0)\n(tar-0.5.1.1 ∧ tar-0.5.1.1-flag-old-bytestring) → (bytestring-0.9 ∨ ... ∨ bytestring-0.9.2.1)\n(tar-0.5.1.1 ∧ ¬ tar-0.5.1.1-flag-old-bytestring) → (bytestring-0.10.0.0 ∨ ... ∨ bytestring-0.11.5.1)\n```\n\ni.e.\n\n```text\n¬ tar-0.5.1.1 ∨ ¬ tar-0.5.1.1-flag-old-bytestring ∨ bytestring-builder-0.10.4.0.2 ∨ ... ∨ bytestring-builder-0.10.8.2.0\n¬ tar-0.5.1.1 ∨ ¬ tar-0.5.1.1-flag-old-bytestring ∨ bytestring-0.9 ∨ ... ∨ bytestring-0.9.2.1\n¬ tar-0.5.1.1 ∨ tar-0.5.1.1-flag-old-bytestring ∨ bytestring-0.10.0.0 ∨ ... ∨ bytestring-0.11.5.1\n```\n\nMore complicated conditions then just single flag, e.g. something like\n\n```cabal\nif flag(old-bytestring) || flag (old-time)\n  build-depends: ...\n```\n\ncan be encoded using an auxiliary variable:\n\n```text\ncondition-aux ↔ (tar-0.5.1.1-flag-old-bytestring ∨ tar-0.5.1.1-flag-old-time)\n(tar-0.5.1.1 ∧ condition-aux) → ...\n```\n\nTurns out SAT encoding of the dependency resolution problem is not very complicated.\n\nProblem size problem\n--------------------\n\nAt the time of writing, there are around 17500 packages and 135000 package-versions (i.e. about 7-8 versions per package in average) on Hackage.\nThat's a lot.\nModern SAT solvers probably can handle this just fine, but we can do better.\n\nThere are *incremental* SAT solvers.\nWhich means that we create a solver instance, add variables and clauses, ask for solution, and then we can  continue adding variables and clauses and solve again.\nThis is in fact quite efficient, as SAT solvers \"learn\" (quite literally) about the problem, and reuse the knowledge in the consequtive searches. \n\nWe can use incrementality to only encode parts of search space we need.\n1. We start by encoding that we need target package. Ask for solution.\n2. Look through solution, find selected package-versions which don't have their dependencies expanded yet, expand them.\n3. If there weren't any package-versions we need to expand, we are done. Otherwise ask for a new solution and go to step two.\n\nUnfortunately [ersatz](https://hackage.haskell.org/package/ersatz) (I'd say go to package for dealing with SAT problems) doesn't support incremental mode of operation so I made a small package of my own: [sat-simple](https://hackage.haskell.org/package/sat-simple).\n\nReal solver\n-----------\n\n`cabal-install` supports having multiple dependency solvers, at least in theory. The dependency solver is \"just\" a function:\n\n```haskell\ntype DependencyResolver loc =\n    Platform -\u003e\n    CompilerInfo -\u003e\n    InstalledPackageIndex -\u003e\n    PackageIndex (SourcePackage loc) -\u003e\n    PkgConfigDb -\u003e\n    (PackageName -\u003e PackagePreferences) -\u003e\n    [LabeledPackageConstraint] -\u003e\n    Set PackageName -\u003e\n    Progress String String [ResolverPackage loc]\n```\n\nThe `Platform` and `CompilerInfo` data is needed to evaluate `if arch(...)`, `if os(...)` and `if impl(..)` conditionals.\nAlso `CompilerInfo` contains the set of known extensions, so package-versions which specify `default-extensions` or `other-extensions` can be\nruled out as well.\n\nThe `InstalledPackageIndex` is the installed packages. Currently these contain the package (versions) bundled with GHC: `base`, `ghc-prim`, `template-haskell` but also a version of `containers`, `transformers` etc.\n\nThe `PackageIndex (SourcePackage loc)` are the source packages, i.e. usually Hackage, but also (local) packages in the project.\nThe `loc` argument is an extra bits for each package (usually a location), which solver doesn't care about, but threads through into result.\n\nThe `PkgConfigDb` are the native libraries which build info is provided by `pkg-config`, i.e. solver takes into account `pkgconfig-depends` definitions.\nIn other words, it can backtrack if `pkgconfig-depends` are not satisfied. Yet, the `extra-libraries` are always assumed to be satifiable.\n(I think it's mainly because the solver interface is *pure*, i.e. no IO. The `pkg-config` database can be preread, but you cannot prepare all libraries that could exist in `extra-libraries`. But if solver interface allowed IO, then solver could also try `extra-libraries`, which would allow to fail early, and not in the building phase if libraries are missing).\n\nThe `(PackageName -\u003e PackagePreferences)` tells which versions for packages to prefer. These are soft-constraints: we can tell solver to avoid using some version.\n\nThe `[LabeledPackageConstraint]` are on the other hand the hard constraints. For example you can have\n\n```cabal\nconstraint: transformers installed\nconstraint: lens ^\u003e=5.2.3\n```\n\nas additional constraints in your `cabal.project`. These are not problematic for SAT encoding.\nWe could either encode them directly, or apply them even before encoding to SAT.\n(`cabal-install`s solver uses them similarly to other constraints, which makes the error reporting more uniform. E.g. you see that it rulled all older `lens` versions due the constraint, they don't simply disapppear).\n\nThe `Set PackageName` ...\n\nImplementation\n--------------\n\nTBW\n\nEvaluation\n----------\n\nTBW\n\nConclusion\n----------\n\n- https://github.com/commercialhaskell/stack/issues/116\n- https://github.com/HakeIO/hake-solver/tree/master\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphadej%2Fcabal-solver-sat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphadej%2Fcabal-solver-sat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphadej%2Fcabal-solver-sat/lists"}