{"id":25474658,"url":"https://github.com/claudiuceia/combine","last_synced_at":"2025-11-05T15:30:27.103Z","repository":{"id":44377340,"uuid":"447379969","full_name":"ClaudiuCeia/combine","owner":"ClaudiuCeia","description":"An implementation of parser combinators for Deno. Very much a work in progress.","archived":false,"fork":false,"pushed_at":"2024-08-14T11:36:18.000Z","size":119,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-02-17T19:45:20.598Z","etag":null,"topics":["deno","parser-combinators","parsers","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ClaudiuCeia.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":"2022-01-12T21:46:24.000Z","updated_at":"2024-10-10T16:12:20.000Z","dependencies_parsed_at":"2024-08-14T12:51:34.246Z","dependency_job_id":"20eadbf3-8785-4127-abf5-6d929829af53","html_url":"https://github.com/ClaudiuCeia/combine","commit_stats":{"total_commits":28,"total_committers":3,"mean_commits":9.333333333333334,"dds":0.1785714285714286,"last_synced_commit":"931432bf165f164c1568370f652c66441d22683c"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ClaudiuCeia%2Fcombine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ClaudiuCeia%2Fcombine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ClaudiuCeia%2Fcombine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ClaudiuCeia%2Fcombine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ClaudiuCeia","download_url":"https://codeload.github.com/ClaudiuCeia/combine/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239462883,"owners_count":19642834,"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":["deno","parser-combinators","parsers","typescript"],"created_at":"2025-02-18T11:30:48.033Z","updated_at":"2025-11-05T15:30:27.067Z","avatar_url":"https://github.com/ClaudiuCeia.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# combine\n\nAn implementation of [parser combinators](https://en.wikipedia.org/wiki/Parser_combinator) for [Deno](https://deno.land/). \n\n## Example\n\n```ts\nimport { \n  seq, \n  str, \n  optional, \n  mapJoin, \n  manyTill, \n  anyChar, \n  space, \n  map \n} from \"https://deno.land/x/combine@v0.0.10/mod.ts\";\n\nconst helloWorldParser = seq(\n  str(\"Hello,\"),\n  optional(space()),\n  mapJoin(manyTill(anyChar(), str(\"!\"))),\n);\n\nconst worldRes = helloWorldParser({\n  text: \"Hello, World!\",\n  index: 0,\n});\n\n/**\n{\n  success: true,\n  ctx: {\n    text: \"Hello, World!\",\n    index: 13\n  },\n}\n*/\n\nconst nameParser = map(helloWorldParser, ([, , name]) =\u003e name);\nconst nameRes = nameParser({\n  text: \"Hello, Joe Doe!\",\n  index: 0,\n});\n\n/**\n{\n  success: true,\n  value: \"Joe Doe!\",\n  ctx: {\n    text: \"Hello, Joe Doe!\",\n    index: 15\n  },\n}\n*/\n```\n\nFor more examples,\n[take a look at tests](https://github.com/ClaudiuCeia/combine/tree/main/src/tests).\n\n## About\n\nA parser combinator is a function that takes several parsers as input, and\nreturns a new parser. [combine](https://github.com/ClaudiuCeia/combine/) defines\na few such combinators depending on how the parsers should be combined,\n[seq](https://github.com/ClaudiuCeia/combine/blob/main/src/combinators.ts#L42)\nwhich takes a list of parser that are applied sequentially,\n[oneOf](https://github.com/ClaudiuCeia/combine/blob/main/src/combinators.ts#L109)\nwhich tries all parsers sequentially and applies the first one that's succesful,\n[furthest](https://github.com/ClaudiuCeia/combine/blob/main/src/combinators.ts#L150)\nwhich tries all parsers and applies the one that consumes the most input\n[and more](https://github.com/ClaudiuCeia/combine/blob/main/src/combinators.ts).\n\nMost included parsers are [LL(1)](https://en.wikipedia.org/wiki/LL_parser), with\nsome notable exceptions such as\n[str](https://github.com/ClaudiuCeia/combine/blob/main/src/parsers.ts#L8) and\n[regex](https://github.com/ClaudiuCeia/combine/blob/main/src/parsers.ts#L274).\nOther LL(k) parsers library are the result of using combinators and are included\nfor convenience, like\n[signed](https://github.com/ClaudiuCeia/combine/blob/main/src/parsers.ts#L259),\n[horizontalSpace](https://github.com/ClaudiuCeia/combine/blob/main/src/parsers.ts#L189)\nand [others](https://github.com/ClaudiuCeia/combine/blob/main/src/parsers.ts).\n\nA couple of\n[common utility functions](https://github.com/ClaudiuCeia/combine/blob/main/src/utility.ts)\nare also included.\n\n## Order and recursion\n\nWhile you can use parsers as shown in the above example, that quickly becomes a\nproblem for some parsing tasks, like DSLs.\n\nTake a simple calculator grammar defined as:\n\n```\nexpr=term, expr1;\nexpr1=\"+\",term,expr1|\"-\",term,expr1|;\nterm=factor, term1;\nterm1=\"*\", factor, term1 | \"/\", factor, term1|;\nfactor=\"(\", expr , \")\" | number;\nnumber=digit , {digit};\ndigit = \"1\"|\"2\"|\"3\"|\"4\"|\"5\"|\"6\"|\"7\"|\"8\"|\"9\"|\"0\";\nsyntax=expr;\n```\n\n`expr` needs to be defined using `term` and `expr1`, so these two parsers need\nto be defined first. But then `expr1` refers to itself which triggers an\ninfinite loop unless we use\n[lazy](https://github.com/ClaudiuCeia/combine/blob/main/src/utility.ts#L29-L31).\n\nAn implementation of the above can be seen in the\n[calculator test](https://github.com/ClaudiuCeia/combine/blob/main/tests/calculator.test.ts).\n\nWe can see that the parsers which depend on each other need to be declared using\na named function as opposed to `addop` and `mulop`. Also, in the `factor` parser\nwe need to use `lazy`, otherwise we'd trigger an infinite mutual recursion\nwhere:\n\n`factor` calls `expression` `expression` calls `factor` ...\n\n### createLanguage\n\nBorrowing a trick from [Parsimmon](https://github.com/jneen/parsimmon), we can use the `createLanguage` function to\ndefine our grammar. This allows us to not worry about the order in which we\ndefine parsers, and we get each parser defined as lazy for free (well, with some\nminor computational cost). You can see a comparison of directly using the parser\nvs `createLanguage` in\n[this benchmark](https://github.com/ClaudiuCeia/combine/blob/main/bench/createLanguage_bench.ts),\nand you can see another example in\n[this other benchmark](https://github.com/ClaudiuCeia/combine/blob/main/bench/lisp_bench.ts).\n\nTyping support for `createLanguage` is not great at the moment. There are two ways to use it:\n\n```ts\nimport { \n  createLanguage, \n  either, \n  str, \n  Parser, \n  UntypedLanguage, \n  number \n} from \"https://deno.land/x/combine@v0.0.10/mod.ts\";\n\n/**\n * Untyped, provide `UntypedLanguage` as a type parameter.\n * This will make all of the grammar consist of Parser\u003cunknown\u003e,\n * but you at least get a mapping for the `self` parameter.\n */\nconst lang = createLanguage\u003cUntypedLanguage\u003e({\n  Foo: (s) =\u003e either(s.Bar /* this is checked to exist */, number()),\n  Bar: () =\u003e str(\"Bar\"),\n});\n\n// Typed\ntype TypedLanguage = {\n  Foo: Parser\u003cstring, number\u003e,\n  Bar: Parser\u003cstring\u003e,\n  // ...\n}\nconst typedLang = createLanguage\u003cTypedLanguage\u003e({\n  Foo: (s) =\u003e either(\n    s.Bar // this is checked to exist with the expected type \n    number(),\n  ),\n  Bar: () =\u003e str(\"Bar\"),\n});\n```\n\nNote that for more complex grammar you generally need some sort of recursion. \nFor those cases, it can be tricky to define the `TypedLanguage`, have a look at \n[this example](https://github.com/ClaudiuCeia/combine/blob/main/tests/language.test.ts)\nfor inspiration.\n\nNote that since this wraps all of the functions in a `lazy()` closure, this also \nbring a small performance hit. In the future we should be able to apply `lazy()` only\nwhere it's needed.\n\n## Performance\n\nPerformance is an inherent challenge for parser combinators. It's easy to create\na parser that performs badly due to backtracking, or by using expensive\ncombinators like\n[furthest](https://github.com/ClaudiuCeia/combine/blob/main/src/combinators.ts#L150).\n\nWith previous Deno versions, the performance of `combine` was abysmal. However,\nthe latest Deno version at the time of writing this (1.36.4) seems to perform\nmuch better than Parsimmon (which I previously recommended as a faster alternative). \nSee [this benchmark](https://github.com/ClaudiuCeia/combine/blob/main/bench/lisp_bench.ts)\nfor a comparison.\n\n```\nbenchmark      time (avg)        iter/s             (min … max)       p75       p99      p995\n--------------------------------------------------------------- -----------------------------\n\n\ncombine        69.01 µs/iter      14,490.2    (46.42 µs … 1.21 ms)  58.88 µs 348.59 µs 405.41 µs\nparsimmon       1.21 ms/iter         828.5   (872.27 µs … 2.87 ms)   1.34 ms   2.07 ms   2.23 ms\n\nsummary\n  parsimmon\n   17.49x slower than combine\n```\n\n## Going forward\n\nThis started out as a learning exercise and it most likely will stay that way\nfor some time, or until it sees some real use. I'm not sure how much time I'll\nbe able to dedicate to this project, but I'll try to keep it up to date with\nDeno releases.\n\n### Major improvement opportunities:\n\n- Tooling: tracing, profiling, etc.\n- Nicer composition of parsers (avoid the\n  [pyramid of doom](https://en.wikipedia.org/wiki/Pyramid_of_doom_(programming)))\n\n\n## License\n\nMIT © [Claudiu Ceia](https://github.com/ClaudiuCeia)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclaudiuceia%2Fcombine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclaudiuceia%2Fcombine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclaudiuceia%2Fcombine/lists"}