{"id":24551995,"url":"https://github.com/mlabs-haskell/snarky-purescript","last_synced_at":"2025-03-16T13:41:39.521Z","repository":{"id":184373440,"uuid":"670903114","full_name":"mlabs-haskell/snarky-purescript","owner":"mlabs-haskell","description":"Groundwork for embedding SnarkyJS into PureScript","archived":false,"fork":false,"pushed_at":"2023-08-17T07:33:30.000Z","size":6952,"stargazers_count":2,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-23T01:19:51.024Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","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/mlabs-haskell.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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}},"created_at":"2023-07-26T05:21:48.000Z","updated_at":"2023-08-22T08:49:31.000Z","dependencies_parsed_at":"2024-04-18T12:25:46.980Z","dependency_job_id":"32f3f41b-5c86-4ca7-9e95-b43fe9bb9922","html_url":"https://github.com/mlabs-haskell/snarky-purescript","commit_stats":null,"previous_names":["gnumonik/snarky-purescript-experiment","mlabs-haskell/snarky-purescript"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlabs-haskell%2Fsnarky-purescript","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlabs-haskell%2Fsnarky-purescript/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlabs-haskell%2Fsnarky-purescript/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlabs-haskell%2Fsnarky-purescript/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mlabs-haskell","download_url":"https://codeload.github.com/mlabs-haskell/snarky-purescript/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243878408,"owners_count":20362431,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":"2025-01-23T01:19:39.958Z","updated_at":"2025-03-16T13:41:39.483Z","avatar_url":"https://github.com/mlabs-haskell.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## WTF? \n\nThis is an extremely experimental PureScript embedding of [SnarkyJS](https://github.com/o1-labs/snarkyjs/tree/main). Its official purpose is a proof-of-concept for a DSL in support of the MLabs [zkPal](https://zkignite.minaprotocol.com/zkignite/dev4dev/draftproposals/suggestion/569) zkIgnite proposal. Its unofficial purpose is facilitate learning about SnarkyJS/zkCircuits and to do some fun things with types :)\n\nIf you're from zkIgnite, please note that this is a research project intended to demonstrate the viability of the concept of a circuit DSL. It does not represent the final design. Though we are rather fond of Purescript, we do not want to force people to learn the language if we can avoid it. \n\n## How to use \n\nReal documentation forthcoming. For now, `Main.purs` contains an example that has been annotated below as a basic tutorial.\n\n**NOTE**: The below tutorial assumes some familiarity with functional programming, and goes into more detail than is necessary to *use* the DSL. Enormous effort has been expended to ensure that typical users never need to think about the scary under-the-hood type level programming that makes this work.\n\n\n``` purescript\nmodule Main where\n\nimport Prelude\nimport Effect (Effect)\nimport Effect.Class (liftEffect)\nimport Effect.Aff (launchAff_)\nimport Effect.Console (log)\n\nimport Data.Foldable (foldM)\n\nimport SnarkyPS.Lib.Prelude\n\n{- Toy game example.\n\n  Game is a guessing game with a board:\n      1 2 3\n   1 | | | |\n     |-----|\n   2 | |x| |\n     |-----|\n   3 | | | |\n\n  \"Private\" player picks a starting position and goal position on a 3x3 grid.\n\n  \"Public\" player tries to guess a series of 4 moves (can pass) to get \n  from the start pos to the goal pos.\n\n  Written to illustrate features, not for elegance/performance/etc.\n-}\n\n{- Overview\n\n  The general idea is that we want to construct a `Circuit pub priv`,\n  where `pub` and `priv` are more-or-less normal PS types,\n  using the `mkCircuit` function.\n\n  Once we have a `Circuit pub priv`, we can (attempt to) generate a proof by\n  applying public and private inputs using the `prove` function. You can think of\n  the type of prove as:\n    `prove :: forall pub priv. Circuit pub priv -\u003e pub -\u003e priv -\u003e Aff (Prove pub priv)`\n\n  The \"magic\" that powers the DSL consists in the argument to the `mkCircuit`\n  function, which has the full type:\n\n    mkCircuit :: forall pub priv pub' priv'\n             . AsFieldsOf pub pub'\n             =\u003e AsFieldsOf priv priv'\n             =\u003e CircuitValue pub\n             =\u003e CircuitValue priv\n             =\u003e (pub' -\u003e priv' -\u003e ZkM Unit)\n             -\u003e Circuit pub priv\n\n  You don't need to understand that type signature to use the DSL, but it\n  illustrates something important: `mkCircuit` constructs a\n  `Circuit pub priv` from a PureScript function that accepts the \n  *AsFieldsOf representations* of the annotated types for arguments.\n\n  The `AsFieldsOf` representation for a type is very easy to determine:\n    a. The `AsFieldsOf` representation of all `Record`s is a `ZStruct` \n       parameterized by a row with the same labels \u0026 the\n       `AsFieldsOf` representation of elements at each label.\n\n    b. The `AsFieldsOf` representation for all `Variant`s is a `ZEnum` \n       parameterized by a row with the same labels \u0026 the\n       `AsFieldsOf` representation of elements at each label.\n\n    c. The `AsFieldsOf` representation of any `FieldLike` type is just that type itself.\n\n  So, informally, the `AsFieldsOf` representation of a type `t` is a ZEnum if \n  `t` is a Variant, a `ZStruct` if `t` is a Record, and just `t` if `t` is a \n  simple FieldLike. The compiler automatically solves the `AsFieldsOf` typeclass \n  due to functional dependencies, so you should never have to annotate types\n  within the Circuit function.\n-}\n\ntype Option a = Variant (some :: a, none :: ZUnit)\n\ntype Position = {x :: U64, y :: U64}\n\ntype Board = {start :: Position, goal :: Position}\n\ntype Move = Variant (up :: ZUnit, down :: ZUnit, left :: ZUnit, right :: ZUnit)\n\ntype Moves = { move1 :: Option Move\n             , move2 :: Option Move\n             , move3 :: Option Move\n             , move4 :: Option Move }\n\n{- NOTES:\n\n  a) You generally want to construct a circuit as I do below. That is:\n    - Give the entire circuit a type signature that indicates the (vanilla PureScript)\n      public/private input types. This is really important - the Circuit DSL has *amazingly* good\n      type inference *so long as you give a top level type*\n\n    - Write a function `pubF -\u003e privF -\u003e ZkM Unit` in the ZkM monad and apply `mkCircuit` \n      to that function\n\n    - Note that pubF and privF are the `AsFieldsOf` representations of  \n      vanilla PureScript types and can be determined in accordance with the above \n      definition  (though type inference should be strong enough that \n      you're never required to annotate them).\n\n      See the comments below for concrete examples of the `AsFieldsOf` \n      representation for input types\n\n  b) Don't worry too much about what the `ZkM` monad is. At the end of the day, \n     it's just a (complicated) trick to emulate a constrained Monad in PureScript. \n     You can think of it loosely as a variation of the `Identity`\n     Monad that lacks a `runIdentity` function. (Under the hood there is a special \n     constrained equivalent to `runIdentity`, but as a user you have no reason to \n     ever extract anything from the Monad)\n-}\ngameCircuit :: Circuit Moves Board\ngameCircuit = mkCircuit $ \\moves board -\u003e do\n      {- startPos :: `ZStruct (x :: U64, y :: U64)`\n\n        This illustrates that `get` always returns the AsFieldsOf representation of the field \n        at its label. (This is why `get` is Monadic but `set` and `over` are pure.)\n      -}\n      startPos \u003c- get @\"start\" board\n\n      {- assertM :: ZkM Assertion -\u003e ZkM Unit\n\n        If you're a functional programmer, `assertM` is just an alias for `Control.Monad.void`\n      -}\n      assertM $ checkPos \"bad start\" startPos\n\n      goal  \u003c- get @\"goal\" board\n\n      assertM $ checkPos \"bad goal\" goal\n\n      {- The type of move1,move2,move3,move4 is:\n\n          `ZEnum (some :: ZEnum (up :: ZUnit, down :: ZUnit, left :: ZUnit, right :: ZUnit), none :: ZUnit)`\n\n        Which is the `AsFieldsOf` representation of `Option Move`. Again, you \n        shouldn't ever need to annotate this, and the DSL is designed in such \n        a way that you should really never have to *think* about the AsFieldsOf \n        representation.\n\n        It may be helpful to add type annotations for debugging if you\n        get a confusing error. (The more type information you provide, \n        the more likely you are to get a *useful* error message.)\n      -}\n      move1 \u003c- get @\"move1\" moves\n      move2 \u003c- get @\"move2\" moves\n      move3 \u003c- get @\"move3\" moves\n      move4 \u003c- get @\"move4\" moves\n\n      {- ZkM is a monad and we can do normal monad things with it, e.g. foldM/mapM/etc -}\n      endPos \u003c- foldM runMove startPos [move1,move2,move3,move4]\n\n      assertM $ checkOutcome goal endPos\n     where\n      {- checkPos :: String -\u003e ZStruct (x :: U64, y :: U64) -\u003e ZkM Assertion -}\n      checkPos msg pos = do\n        posX \u003c- get @\"x\" pos\n        posY \u003c- get @\"y\" pos\n        let p x = (x #\u003e u64 0) \u0026\u0026 (x #\u003c= u64 3) -- The #-prefixed Ord operators return Bool (the circuit value) and not Boolean (the PS value)\n        pure $ assertTrue msg (p posX \u0026\u0026 p posY) -- Bool has a HeytingAlgebra instance so you can use `not/\u0026\u0026/||` and so on to combine Bool values\n\n      {- checkOutcome :: ZStruct (x :: U64, y :: U64) -\u003e ZStruct (x :: U64, y :: U64) -\u003e ZkM Assertion -}\n      checkOutcome goalCoords endCoords = do\n        gX \u003c- get @\"x\" goalCoords\n        gY \u003c- get @\"y\" goalCoords\n\n        eX \u003c- get @\"x\" endCoords\n        eY \u003c- get @\"y\" endCoords\n\n        pure $ assertTrue \"player loses\" (gX #== eX \u0026\u0026 gY #== eY)\n\n      {-\n      runMove :: ZStruct (x :: U64, y :: U64)\n              -\u003e ZEnum (some :: ZEnum (up :: ZUnit, down :: ZUnit, left :: ZUnit, right :: ZUnit), none :: ZUnit)\n              -\u003e ZkM (ZStruct (x :: U64, y :: U64))\n\n      **SEE \"NOTES ON PATTERN MATCHING\" BELOW\n      -}\n      runMove start mabMove = pure $ caseOn start mabMove {\n                none: zUnit ==\u003e  start,\n                some: {\n                   up: zUnit ==\u003e over @\"y\" minusOne start,\n                   down: zUnit ==\u003e over @\"y\" plusOne start,\n                   left: zUnit ==\u003e over @\"x\" minusOne start,\n                   right: zUnit ==\u003e over @\"x\" plusOne start\n                  }\n                }\n        where\n          {- You can add/subtract/multiply U64s using normal arithmetic operators\n             since U64 has Semiring/Ring instances\n          -}\n          minusOne x =  x - (u64 1)\n          plusOne x = x + (u64 1)\n\ntestBoard :: Board\ntestBoard = {start: {x: u64 2, y: u64 2}, goal: {x: u64 2, y: u64 1}}\n\n{- Sums and Enumerations are represented in PS as Data.Variant Variants (with a `ZEnum` AsFields representation)\n\n  For convenience, users can construct Variants using the provided `inj` and `inj_` functions:\n    `inj` is just like Data.Variant.Inj except it uses a type application instead of a Proxy Argument.\n\n    `inj_` is just like `inj` for labels with a `ZUnit` argument (so that you don't\n    have to manually pass a `zUnit` arg)\n-}\ntestMoves :: Moves\ntestMoves = {move1: inj @\"some\" up , move2: nullMove, move3: nullMove, move4: nullMove}\n  where\n    nullMove :: Option Move\n    nullMove = inj_ @\"none\"\n    up :: Move\n    up = inj_ @\"up\"\n\nmain :: Effect Unit\nmain =  launchAff_  do\n  proof \u003c- prove gameCircuit testMoves testBoard\n  liftEffect $ debug proof\n\n{- NOTES ON PATTERN MATCHING\n\n  This DSL provides a *very limited* form of pattern matching. If you're coming \n  from a language with sophisticated pattern matching (any functional language, \n  Rust, etc) you likely won't have a good intuition for the limitations, \n  so here's an overview of how matching works here:\n\n  The function you use to pattern match on Circuit values is (terrifying type sig):\n\n    `caseOn :: forall @t @m @r. CircuitValue r =\u003e CircuitValue t =\u003e Matchable t m r =\u003e r -\u003e t -\u003e m -\u003e r`\n\n  `r` is the result type of the match, and you *must* provide a default result \n  value when matching. If your Matcher Expression (see below) does not yield \n  any matches, the default result value is returned.\n\n  `t` is the type of the value you wish to match on.\n\n  `m` is a Matcher Expression. Its type depends upon the type of `t`, in the following manner:\n\n     - If `t` is a ZEnum (i.e. the Circuit representation of a Variant), then `m`\n       should be a *Record* with the same labels as the ZEnum (or original variant), \n       where each label of that record should be associated with a\n       record field that contains a Matcher Expression for the corresponding \n       Variant/ZEnum label.\n\n     - If `t` is ZUnit, `m` should have the type `Match ZUnit r`. The `==\u003e` operator\n       constructs a match, so if you are matching on a ZUnit, you will \n       usually construct the match like: `zUnit ==\u003e result`.\n\n     - If `t` is a FieldLike *other than ZUnit*, `m` should have the type \n       `Array (Match tx r)`, where `tx` is a PureScript expression of a type \n       suitable for matching with `t`. As of right now, this means:\n       - The Matcher Expression for `U64` is `Array (Match Int r)`\n       - The Matcher Expression for 'Bool' is `Array (Match Boolean r)`\n       - The Matcher Expression for 'Field' is `Array (Match Field r)`\n\n     - At the moment, you can't match on `ZStruct`s. There's no reason why \n       that couldn't be implemented, but it seems better to require users to \n       extract fields with `get` than to match on an entire ZStruct.\n\n  `runMove` illustrates matcher expressions for Variants and ZUnit. If we wanted \n   to match on a `Bool` field, we could do it like:\n      caseOn default (myBool :: Bool) [\n        true ==\u003e (...),\n        false ==\u003e (...)\n      ]\n\n  Similarly, if you wanted to match on a `U64` field, you'd do it like:\n     caseOn default (myU64 :: U64) [\n       0 ==\u003e (...),\n       1 ==\u003e (...),\n       2 ==\u003e (...)\n     ]\n\n  The U64 example reveals one fundamental limitation of pattern matching in a Circuit:\n    *** All matches must be literal matches ***\n\n  If you're used to Haskell/PureScript/Rust/etc, you're likely familiar with \n  *pattern variables*  (even if you don't know they're called that). E.g., \n  in Haskell, you can do things like\n    case (x :: Maybe Int) of\n      Just y -\u003e y + 1\n      Nothing -\u003e 0\n      \n  Where `y` in that example is a *pattern variable* on the left side of the \n  arrow that gets \"transformed into\" an Expression Variable on the right hand \n  side. ***You can't use pattern variables in the DSL***. As far as I can tell,\n  this is an inherent limitation of the proof system upon which SnarkyJS is built \n  (though I am continuing to conduct research in the hopes that I will discover a \n  way around the limitation).\n\n  Note a consequence of this limitation: If you have a\n\n    `a :: ZEnum (some :: x, none :: ZUnit)`\n\n  You can *only extract a value of type `x` if you can provide a literal match \n  for ALL values of type `x`*. `caseOn` is ultimately syntactic sugar over `zkIf`, \n  which is our translation of SnarkyJS's `Provable.if``. In most circumstances, \n  it is only *useful* to match on (small) finitely-enumerable types (such as ZEnums\n  that correspond to user-defined Variants), or primitive FieldLikes \n  that can be finitely enumerated (e.g. Bool).\n\n  The above considerations entail that there is no exhaustiveness check - \n  You will get the default value if you miss a match branch in your \n  Matcher Expression.\n-}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmlabs-haskell%2Fsnarky-purescript","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmlabs-haskell%2Fsnarky-purescript","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmlabs-haskell%2Fsnarky-purescript/lists"}