{"id":22946628,"url":"https://github.com/mmower/epic","last_synced_at":"2025-04-01T22:40:31.557Z","repository":{"id":66365698,"uuid":"331644980","full_name":"mmower/epic","owner":"mmower","description":"Parser combinator library for Elixir projects","archived":false,"fork":false,"pushed_at":"2021-07-01T10:17:52.000Z","size":47,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-07T14:45:41.008Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mmower.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2021-01-21T13:59:30.000Z","updated_at":"2021-07-01T10:17:55.000Z","dependencies_parsed_at":"2023-02-20T16:15:40.219Z","dependency_job_id":null,"html_url":"https://github.com/mmower/epic","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmower%2Fepic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmower%2Fepic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmower%2Fepic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mmower%2Fepic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mmower","download_url":"https://codeload.github.com/mmower/epic/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246724637,"owners_count":20823542,"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":"2024-12-14T14:47:37.144Z","updated_at":"2025-04-01T22:40:31.545Z","avatar_url":"https://github.com/mmower.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Epic\n\nVersion: 0.2 (basic, working)\n\nAuthor: Matt Mower \u003cmatt@theartofnavigation.co.uk\u003e\n\n## Introduction\n\nEpic is a Parser Combinator library. That is, it is a library that you can use to build\na parser assembled out of smaller parsers. For example if you wanted to parse expressions\nlike:\n\n    1+2\n    3*4\n    5/6\n    7-8\n\nyou might imagine building a combinator parser like:\n\n    integer -\u003e operator -\u003e integer (conceptually)\n\n    sequence([integer(), operator(), integer()])\n\nwhere `integer` is a parser responsible for parsing digitals and `operator` is a parser\nresponsible for parsing symbols like `+` and `*`.\n\nFor such a simple input grammar you might reach for a regular expression and that would be quite a reasonable choice. However, as the complexity of the inputs increases, regular expressions become less easy to understand and maintain.\n\nFor example to parse an input such as:\n\n    Game {\n      @title = \"See the galaxy on less than five Altarian dollars a day\"\n      @author = \"Matt Mower\"\n      @author_email = \"matt@theartofnavigation.co.uk\"\n      @uuid = \"FCCBAE02-3FD5-45C4-A56B-ECEA30B31610\"\n\n      Act earth_destroyed {\n        @title = \"The first act\"\n        @protagonist = #arthur_dent\n        @antagonists = [#ford_prefect, #mr_prosser, #vogon_captain]\n\n        Scene yellow_bulldozer {\n          @opening_dialogue = #ford_prefect \"Hello Arthur\"\n          …\n        }\n      }\n\n      Act earth_mark_two {\n        …\n      }\n    }\n\n    and so on…\n\nYou need a parser.\n\nParser combinators, like Epic, allow for the \"layering up\" of simple parsers into more complex parsers. This\nhelps to clarify intent. As well it makes it easy to transform parsed terms into more useful forms. An imagined\nform of parser combinator for the above input (ignoring whitespace issues) might look like:\n\n  game =\n    sequence([\n      literal(\"Game\"),\n      literal(\"{\",\n      many(choice([attribute, act])),\n      literal(\"}\")\n    ])\n\n  act =\n   sequence([\n     literal(\"Act\"),\n     literal(\"{\"),\n     many(choice([attribute, scene])),\n     literal(\"}\")\n   ])\n\n   scene =\n    sequence([\n      literal(\"Scene\"),\n      literal(\"{}\"),\n      many(choice[attribute, …]),\n      literal(\"}\")\n      })\n    ])\n\n    attribute =\n      literal(\"@\")\n      |\u003e identifier\n      |\u003e literal(\"=\")\n      |\u003e value\n\n    and so on…\n\nEpic provides a number of helpful low-level parsers like `literal`, `sequence`, `many`, and `choice` which can be combined, DSL like, to form a more complex parser for your input grammar. Additionally it provides useful transformers that can convert parsed results into maps and records.\n\nLastly, because Epic was created to parse a human authored format, some effort has been made to ensure that\nEpic parsers can be made to report human readbale error messages with useful positional information.\n\n## Progress\n\nI've implemented most of the basic parsing primitives so you can build a functional parser. However there are issues:\n\n### Error handling\n\nIt's a stated ambition of Epic that generated parsers provide decent, human understandable, error messages\nwhen things go wrong. Epic tracks line \u0026 column information which is a start but this remains more of an\nambition than actuality.\n\n### chaining of parsers\n\nIn NimbleParsec you can write:\n\n    string(\"{\"}) |\u003e identifier |\u003e string(\"}\") |\u003e …\n\nbecause all the combinators take another combinator as the first parameter and \"chain\" them along collecting\nthe results.\n\nI wasn't sure how to implement this (I found collecting results using this approach challenging) so I opted to be more explicit and introduce the `sequence` parser. So you would write the above example as:\n\n    sequence([char(?{}), identifier(), char(?})])\n\nI think syntactically it's a wash as an example like:\n\n    act =\n        string(\"Act\")\n        |\u003e replace(:act)\n        |\u003e ignore(whitespace)\n        |\u003e concat(id)\n        |\u003e ignore(whitespace)\n        |\u003e ignore(obrace)\n        |\u003e ignore(whitespace)\n        |\u003e optional(attributes)\n        |\u003e ignore(whitespace)\n        |\u003e concat(scene)\n        |\u003e repeat(ignore(whitespace) |\u003e concat(scene))\n        |\u003e ignore(whitespace)\n        |\u003e ignore(cbrace)\n        |\u003e wrap\n\nbecomes\n\n    act = sequence([\n      literal(\"Act\") |\u003e replace(:act),\n      ignore(whitespace),\n      id,\n      ignore(whitespace),\n      ignore(obrace),\n      ignore(whitespace),\n      optional(attributes),\n      ignore(whitespace),\n      many(scene),\n      ignore(whitespace),\n      ignore(cbrace)\n    ])\n\nNote that, in some cases, it still makes sense to chain parsers although I don't think\n\n    replace(literal(\"Act\"), :act)\n\nactually reads any worse.\n\nUnder the hood `literal` is implemented using the `sequence` and `char` parsers by mapping characters of the string\ninto equivalent char parsers strung together with sequence.\n\n### ignoring unwanted input\n\nNimbleParsec has the `ignore` combinator that discards any results. Likewise Epic has an `ignore` parser that returns a type of match that the `many` and `sequence` parsers will discard when collecting results.\n\n### lookahead\n\nI never quite figured out lookahead using NimbleParsec and I'm not sure what to do about it here although I know it can be important.\n\n### efficiency\n\nNimbleParsec is fast. I expect Epic won't be. I will probably care about this last, if at all.\n\n## Background\n\nI wanted a parser for a moderately complicated text format. I came across [NimbleParsec](https://github.com/dashbitco/nimble_parsec) which is a parser combinator library by the author of Elixir, José Valim.\n\nNimbleParsec is pretty easy to get started with and, by all accounts, blazingly fast because José knows all\nthe right tricks. Unfortunately when it came to error handling I couldn't find any [good examples or guidance](https://elixirforum.com/t/can-you-help-me-understand-how-to-implement-nimbleparsec-error-handling/36637). If\nyour context is consuming largely machine-generated inputs where performance outweighs quality of error reporting I think this would be a great choice. For example, if I were writing an HTTP protocol parser I'd use NimbleParsec.\n\nSaša Jurić sent me a link to a video where he [builds up parser combinators from the ground up](https://www.youtube.com/watch?v=xNzoerDljjo). This helped solidify my understanding of the parser combinator approach and\nI realised I could build my own library and try to focus on those areas where I was struggling to make NimbleParsec work for me.\n\nHence Epic was born.\n\nThe first decision I made was to use a `Context` record to maintain the parser state and have all parsers\nconsume and return contexts. The context includes the parser input. I also introduced `Position` to manage\npositional information and `Match` to manage the terms being matched, including storing the match position\nseparately from the parser position.\n\nI do not recommend using this library as it is probably incomplete and may not work in\nall cases and it's raison d'etre (better error handling) is still a matter of conjecture. However,\nif you want to learn about parser combinators and follow-along I suspect my code will be much\neasier to understand and reason about than the NimbleParsec source.\n\nIf you are more interested in performance than error handling then NimbleParsec is the best game in town\nas of the the time of writing.\n\n## Installation\n\nIf [available in Hex](https://hex.pm/docs/publish), the package can be installed\nby adding `epic` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:epic, \"~\u003e 0.1.0\"}\n  ]\nend\n```\n\nDocumentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)\nand published on [HexDocs](https://hexdocs.pm). Once published, the docs can\nbe found at [https://hexdocs.pm/epic](https://hexdocs.pm/epic).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmower%2Fepic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmmower%2Fepic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmmower%2Fepic/lists"}