{"id":18847675,"url":"https://github.com/everythingfunctional/parff","last_synced_at":"2026-01-25T07:03:58.603Z","repository":{"id":146073842,"uuid":"476483197","full_name":"everythingfunctional/parff","owner":"everythingfunctional","description":"The foundations of a functional style parser library","archived":false,"fork":false,"pushed_at":"2022-05-10T21:27:06.000Z","size":205,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-23T00:33:05.356Z","etag":null,"topics":["fortran","parser","parser-combinators","parser-library"],"latest_commit_sha":null,"homepage":"","language":"Fortran","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/everythingfunctional.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}},"created_at":"2022-03-31T21:24:33.000Z","updated_at":"2022-04-06T20:58:02.000Z","dependencies_parsed_at":"2023-09-21T19:18:35.092Z","dependency_job_id":"5d96311f-8e81-452c-8aee-a5ce90c60b59","html_url":"https://github.com/everythingfunctional/parff","commit_stats":{"total_commits":113,"total_committers":5,"mean_commits":22.6,"dds":0.504424778761062,"last_synced_commit":"dbce6a04992fbc1e2d13b6c633480d3cd4bbe35c"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/everythingfunctional/parff","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everythingfunctional%2Fparff","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everythingfunctional%2Fparff/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everythingfunctional%2Fparff/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everythingfunctional%2Fparff/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/everythingfunctional","download_url":"https://codeload.github.com/everythingfunctional/parff/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/everythingfunctional%2Fparff/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28747308,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-25T05:12:38.112Z","status":"ssl_error","status_checked_at":"2026-01-25T05:04:50.338Z","response_time":113,"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":["fortran","parser","parser-combinators","parser-library"],"created_at":"2024-11-08T03:09:12.754Z","updated_at":"2026-01-25T07:03:58.588Z","avatar_url":"https://github.com/everythingfunctional.png","language":"Fortran","funding_links":[],"categories":[],"sub_categories":[],"readme":"parff\n=====\n\n[![pipeline status](https://gitlab.com/everythingfunctional/parff/badges/main/pipeline.svg)](https://gitlab.com/everythingfunctional/parff/commits/main)\n\nParser for Fortran. The foundations of a functional style parser combinator library.\n\nBy using a library like this, it is possible to implement parsers for complex\nformats by composing independent little pieces. A good example of a project that\nhas made successful use of this library is\n[jsonff](https://gitlab.com/everythingfunctional/jsonff).\n\n## Tutorial\n\nA frequent requirement in software is the parsing of some textual data\ninto the internal representation required by our program.\nThis is often more difficult than it may at first appear.\nIt usually results in some hand-rolled, state machine base implementation,\nspecifically coded for the custom format we're using.\nAnd the difficulty in writing some reusable parsing library is that\nit cannot know ahead of time all the data types and formats that\nmay need to be parsed.\n\nHowever, this is a problem that has (I think) finally been quite satisfactorily solved by parser combinator libraries.\nThe most user friendly and well known examples come from functional programming languages.\nThere are great tutorials for those available, for example\n[this one using F#](https://fsharpforfunandprofit.com/posts/understanding-parser-combinators/),\nand [this one for Haskell's parsec](https://hasura.io/blog/parser-combinators-walkthrough/).\n\nThese implementations make use of features of those languages that Fortran does not have,\ni.e. lambda functions, closures, currying, and type inference/unification.\nThis makes these libraries easier to implement and more convenient to use,\nbut do not make it impossible to implement something similar in Fortran, as we shall now demonstrate.\n\n### What is a Parser?\n\nAt it's most basic, a parser is a function that takes some string,\nand based on its contents, produces a value.\nIn Fortran, that would look something like the following:\n\n```Fortran\nabstract interface\n  function parser_i(string) result(output)\n    character(len=*), intent(in) :: string\n    ??? :: output\n  end function\nend interface\n```\n\nAs you can see, we don't yet know what the type of the output should be.\nThere is another wrinkle as well.\nWhat if the string doesn't contain a valid representation of the type of output we'd like to produce?\nWe'd like the function to produce either an output or an error.\nType inference, pattern matching and higher kinded types allow functional languages to solve this problem rather easily and conveniently,\nbut this is something we can address in Fortran, albeit in a slightly more clunky way,\nby having our function return something like the following:\n\n```Fortran\ntype :: parser_output_t\n  type(error) :: error\n  class(*) :: output\n  logical :: ok ! was parsed correctly\nend type\n```\n\nAn example of what that might look like is as follows.\nSuppose we would like to parse the letter \"A\".\nSuch a parser might look like:\n\n```Fortran\nfunction parse_a(string) result(output)\n  character(len=*), intent(in) :: string\n  type(parser_output_t) :: output\n\n  if (string == \"A\") then\n    output%ok = .true.\n    output%output = \"A\"\n  else\n    output%ok = .false.\n    output%error = \"Expected an A\" ! Assuming this works for our `error` type\n  end if\nend function\n```\n\n### What is a Combinator?\n\nSo why should I bother conforming to that interface? What does this buy me?\nLet's say you also have a function `parse_b`,\nbut now I need to parse an \"A\" and then a \"B\".\nShould we write a new function `parse_a_then_b`?\nWhy can't I just use the two parsers I already have?\nThat's where combinators come in.\n\nBut first, we need to address some additional complexity in our interface.\nPartly this comes in the interest of being able to provide more meaningful error messages.\nIf parser fails, we'd like to be able to say where in the string the problem occurred.\nWe can enable this by passing our current position along to the parser.\nWe'll package our string and position into a derived type like the following for convenience.\n\n```Fortran\ntype :: state_t\n  type(varying_string) :: input ! we'll start using this instead of raw characters\n  type(position_t) :: position ! the internals of this aren't particularly important\nend type\n```\n\nNow, we're probably going to want to put \"A\" and \"B\" together as the output from our combined parser.\nThis is such a common pattern, where one parser needs access to the output of the parser before it,\nthat we'll need to accommodate this as part of our library of combinators.\nThus, we'll also have the following interface.\n\n```Fortran\nabstract interface\n  function then_parser_i(previous, state_) result(result_)\n    class(parsed_value_t), intent(in) :: previous\n    type(state_t), intent(in) :: state_\n    type(parser_output_t) :: result_\n  end function\nend interface\n```\n\nAt this point, we do still need to write a new function, `then_parse_b`,\nbut it can call `parse_b` rather than re-implement it's logic,\nand can be used after parsers other than `parse_a`.\nIt looks something like the following.\n\n```Fortran\nfunction then_parse_b(previous, state_) result(result_)\n  class(parsed_value_t), intent(in) :: previous\n  type(state_t), intent(in) :: state_\n  type(parser_output_t) :: result_\n\n  result_ = parse_b(state_)\n  if (result_%ok) then\n    select type (previous)\n    type is (parsed_character_t)\n      select type (b =\u003e result_%parsed)\n      type is (parsed_character_t) ! unfortunately these select types are necessary\n        result_ = result_%with_parsed_value(parsed_string_t(previous%char // b%char))\n      end select\n    end select\n  end if\nend function\n```\n\nGiven this, we now can now implement our `parse_a_then_b` (if we'd actually like to have it as it's own function)\nwith a single line, `result_ = sequence(parse_a, then_parse_b, state)`.\n\nThe `sequence` function is what is called a combinator, it combines two parsers,\nand has an implementation like the following.\n\n```Fortran\nfunction sequence(parser1, parser2, state) result(result_)\n  procedure(parser_i) :: parser1\n  procedure(then_parser_i) :: parser2\n  type(state_t), intent(in) :: state\n  type(parser_output_t) :: result_\n\n  result_ = parser1(state)\n  if (result_%ok) then\n    result_ = parser2(result_%parsed, result_%new_state)\n  end if\nend function\n```\n\n### Simple Parsers\n\n(effectively) Every parser is implemented in terms of the basic parser, `satisfy`.\n`satisfy` has the following interface, with the basic logic of;\nif the first character in the given string produces true with the given logical function then the output is the first character,\notherwise the result is an error stating that it found the first character.\n\n```Fortran\nfunction satisfy(matches, state) result(result_)\n  procedure(match_i) :: matches\n  type(state_t), intent(in) :: state\n  type(parser_output_t) :: result_\nend function\n\nfunction match_i(char_) result(matches)\n  character(len=1), intent(in) :: char_\n  logical :: matches\nend function\n```\n\nThis is where the unfortunate clumsiness of this approach in Fortran presents itself.\nYou'll notice that the `satisfy` function does not conform to either the `parser_i` or `then_parser_i` interfaces,\nand thus is not able to be used as one of the arguments to any of the combinators.\nIn fact, this is a problem for most of the simple parsers, as they generally require an additional parameter.\nOther languages can get around this problem by using currying\n(a function that is not passed enough arguments becomes a function that takes only the remaining arguments),\nor by using closures, where a function returns another function that can refer to the original argument.\nUnfortunately, neither of these techniques are available in Fortran.\nThis means that many of the primitive/simple parsers must be wrapped in intermediate functions.\nIn practice, while this adds to the verbosity, it is rarely too much of a burden.\n\nOther simple parsers provided are:\n\n* `parse_nothing` - consumes none of the string, and returns a `NOTHING` value\n* `parse_end_of_input` - returns a `NOTHING` value, but only if there is no string left to parse\n* `parse_char` - parse only the given character\n* `parse_string` - parse exactly the given string\n* `parse_digit` - returns the next character if it is one of `0123456789`\n* `parse_integer`/`parse_rational` - returns an `integer`/`double precision` value, consumes as much of the string as can be parsed (at least 1 character)\n* `parse_whitespace` - returns the next character if it is space, tab, newline or carriage return\n\n### Combinators\n\nWe've already seen one combinator, the `sequence` function, but there are a variety of them.\n\nOne that is frequently used, and is essential for providing more helpful error messages, is the `with_label` function.\nIt doesn't combine parsers, but rather, if the given parser fails, gives a meaningful statement about what was expected.\nNotice that the `satisfy` parser does not know what was expected, and thus cannot say.\n\nThe `sequence` combinator allows combining the results of running one parser after another.\nThe `drop_then` and `then_drop` also allow running one parser after another, but discarding one of the results.\n`return_` uses the given value as the output without looking at the input.\n\n`optionally` will always succeed, returning nothing if the given parser failed.\n`either` will attempt to use the second parser if the first one fails.\n\nThe following parsers allow using a parser multiple times:\n\n* `many` - return an array of results, 0 or more, as long as the parser continues to succeed\n* `many1` - same as `many`, but fails if 0 results are produced\n* `many_with_separator` - same as `many`, but uses the second parser between each use of the first\n* `many1_with_separator` - same as `many_with_separator`, but fails if 0 results are produced\n* `repeat_` - return an array of results, running the parser exactly the given number of times\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feverythingfunctional%2Fparff","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feverythingfunctional%2Fparff","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feverythingfunctional%2Fparff/lists"}