{"id":19701980,"url":"https://github.com/jnguyen1098/stately","last_synced_at":"2025-09-18T00:53:18.171Z","repository":{"id":82046897,"uuid":"353141231","full_name":"jnguyen1098/stately","owner":"jnguyen1098","description":"Single-header generic finite-state machine library for C","archived":false,"fork":false,"pushed_at":"2021-04-11T09:38:59.000Z","size":54,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-27T16:16:47.275Z","etag":null,"topics":["c","finite-state-machine","header-only","single-header"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jnguyen1098.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-03-30T21:05:03.000Z","updated_at":"2022-04-01T07:37:35.000Z","dependencies_parsed_at":null,"dependency_job_id":"a41012b3-c1ae-43db-80a7-c0d73300f34f","html_url":"https://github.com/jnguyen1098/stately","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jnguyen1098/stately","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jnguyen1098%2Fstately","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jnguyen1098%2Fstately/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jnguyen1098%2Fstately/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jnguyen1098%2Fstately/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jnguyen1098","download_url":"https://codeload.github.com/jnguyen1098/stately/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jnguyen1098%2Fstately/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":275691197,"owners_count":25510501,"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","status":"online","status_checked_at":"2025-09-17T02:00:09.119Z","response_time":84,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["c","finite-state-machine","header-only","single-header"],"created_at":"2024-11-11T21:12:29.485Z","updated_at":"2025-09-18T00:53:18.155Z","avatar_url":"https://github.com/jnguyen1098.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\r\n  \u003cimg src=\"https://i.imgur.com/scNvoFm.png\"\u003e\r\n\u003c/p\u003e\r\n\r\n# stately\r\n\r\nPolymorphic template-based finite-state machine library written in C, delivered through a single header file. I made this for my game programming class (we had to implement state machine AI) and figured I'd probably make good use of it in other projects, so here it is.\r\n\r\nThanks to C's initialization quirks, trap (rejecting) states are handled automatically, and if you use `char` is the input medium, invalid inputs are also automatically handled.\r\n\r\nStates are implemented internally as `int`s, but as you see in the `examples` folder, `enum`s can (and should) be used for readability.\r\n\r\nA `state_machine` `struct` is declared as follows:\r\n\r\n```c\r\nstruct state_machine {\r\n    int curr_state;\r\n    int (*map)(const void *);\r\n    int state_table[MAX_STATES][MAX_ALPHABET_SIZE + 1];\r\n};\r\n```\r\n\r\nWhere:\r\n\r\n* `curr_state` is the current state of the machine\r\n\r\n* `map()` is a template function allowing the caller to map an arbitrary input to a state (`int`). This is analogous to the `cmp()` parameter in libc `qsort`. Just like `qsort`'s `cmp()`, `map()` takes in a `const void *` and outputs an `int`.\r\n\r\n* `state_table[MAX_STATES][MAX_ALPHABET_SIZE + 1]` is the table used to map the states to each other by means of transitions. It is a 2-dimensional array of `int`s, each row representing the possible transitions from each state and each column representing the input symbol it takes in.\r\n\r\nIn actually manipulating the machine, the following interface is exposed (as macros):\r\n\r\n* `SET_STATE(machine, state)` will set the machine's `curr_state` to argument `state`\r\n\r\n* `GET_STATE(machine)` will retrieve the machine's `curr_state`\r\n\r\n* `GET_NEXT_STATE(machine, input)` will feed the machine argument `input`, mutate its `curr_state` member and then return the `curr_state` just as you would with `GET_STATE(machine)`\r\n\r\n* `SUPPOSE_STATE(machine, state, input)` is a purely functional macro that returns a given next state without changing or relying on the current state (`curr_state`). You specify the machine, the _supposed_ state, and the hypothetical input you would give it, and it would give you the _hypothetical_ output in return.\r\n\r\nTaking a look at a simple example, we examine this DFA program that feeds a string to the finite-state machine character-by-character:\r\n\r\n```c\r\n   /***************************************\r\n    *  DFA that accepts either the empty  *\r\n    *  string, or any sequence of 1s.     *\r\n    *                                     *\r\n    *       1                    0,1      *\r\n    *    +-----+               +----+     *\r\n    *    |     |               |    |     *\r\n    *    |    \\|/              |   \\|/    *\r\n    * +--+---------+       +---+--------+ *\r\n    * |            |       |            | *\r\n    * | Accepting  |       |    TRAP    | *\r\n    * |            |       |            | *\r\n    * +-----+------+       +------------+ *\r\n    *       |                    /|\\      *\r\n    *       |                     |       *\r\n    *       |                     |       *\r\n    *       +---------------------+       *\r\n    *                 0                   *\r\n    **************************************/\r\n\r\n    enum input { INVALID, ZERO_CHAR, ONE_CHAR };\r\n    enum state { TRAP, ACCEPTING };\r\n\r\n    const char char_map[128] = {\r\n        ['0'] = ZERO_CHAR,\r\n        ['1'] = ONE_CHAR,\r\n    };\r\n\r\n    int map_chr(const void *chr) {\r\n        return char_map[(int)*(const char *)chr];\r\n    }\r\n\r\n    struct state_machine machine = {\r\n       \r\n        // Start state\r\n        .curr_state = ACCEPTING,\r\n\r\n        // Input mapper\r\n        .map = map_chr,\r\n\r\n        // States\r\n        .state_table = {\r\n\r\n            // Reject state transitions\r\n            [TRAP] = {\r\n                [INVALID]   = TRAP,\r\n                [ZERO_CHAR] = TRAP,\r\n                [ONE_CHAR]  = TRAP,\r\n            },\r\n\r\n            // Accept state transitions\r\n            [ACCEPTING] = {\r\n                [INVALID]   = TRAP,\r\n                [ZERO_CHAR] = TRAP,\r\n                [ONE_CHAR]  = ACCEPTING,\r\n            }\r\n\r\n        }\r\n\r\n    };\r\n```\r\n\r\nExamining the code bit-by-bit, first we see two enum declarations. One for input, one for state. The fact that we have two declarations is done for readability. In reality all we need are the labels.\r\n\r\n```c\r\n    enum input { INVALID, ZERO_CHAR, ONE_CHAR };\r\n    enum state { TRAP, ACCEPTING };\r\n```\r\n\r\nHere we map characters to states. When we iterate a string, we feed individual characters to the DFA, and each character corresponds to an input. For the sake of readability and maintaining a clean namespace, it is recommended to map `char`s to states, and denote said states using `enum`s. Notice how the `char_map` declaration uses subscripted `[]` notation to initialize the array. Doing so allows us to initialize individual elements in an array without regard to order.\r\n\r\n```c\r\n    const char char_map[128] = {\r\n        ['0'] = ZERO_CHAR,\r\n        ['1'] = ONE_CHAR,\r\n    };\r\n```\r\n\r\nOn a tangential example, declaring\r\n\r\n```c\r\nconst int arr[8] = {\r\n    [3] = 100;\r\n};\r\n```\r\n\r\nis equivalent to\r\n\r\n```c\r\nconst int arr[8] = {\r\n    0, 0, 100, 0, 0, 0, 0, 0\r\n};\r\n```\r\n\r\nas initializing an array zeroes the other elements. Doing\r\n\r\n```c\r\nconst int arr[8];\r\n```\r\n\r\ninitializes none of the values and results in garbage. Looking back at the original snippet of code, it is equivalent to\r\n\r\n```c\r\n    const char char_map[128] = {\r\n        [48] = ZERO_CHAR,\r\n        [49] = ONE_CHAR,\r\n    };\r\n```\r\n\r\nas `'0'` = `48` and `'1'` = `49`. Note an interesting quirk with C's initialization of arrays. Earlier it was said that when you initialize a C array, the rest of the elements are zero'd. In the particular case of a `char` array, by declaring an array of 128 `char`s (i.e., covering the entire range of ASCII characters), we can pick the characters to support as valid inputs (thanks to the readability of `enums`) and then leave the rest of them as invalids. By declaring our `enum` with `INVALID` as the first label, we default our `char_map` to `INVALID` (as `enum`s in C always start at 0). This means in our declaration, we tell stately that `'0'` (`48`) and `'1'` (`49`) are the only legal values: the rest are illegal (`0`/`'\\0'`) and denoting them can be delegated to the `INVALID` label.\r\n\r\nNow we attach `char_map` to the `map()` template function:\r\n\r\n```c\r\n    int map_chr(const void *chr) {\r\n        return char_map[(int)*(const char *)chr];\r\n    }\r\n```\r\n\r\nIt is pretty mundane as we are just returning the state associated with the `char` in the `char_map`, but when you start working with `struct`s as machine inputs, the `map()` function starts becoming more useful.\r\n\r\nNow we look at the actual initialization of the FSA:\r\n\r\n```c\r\n    struct state_machine machine = {\r\n       \r\n        // Start state\r\n        .curr_state = ACCEPTING,\r\n\r\n        // Input mapper\r\n        .map = map_chr,\r\n\r\n        // States\r\n        .state_table = {\r\n\r\n            // Reject state transitions\r\n            [TRAP] = {\r\n                [INVALID]   = TRAP,\r\n                [ZERO_CHAR] = TRAP,\r\n                [ONE_CHAR]  = TRAP,\r\n            },\r\n\r\n            // Accept state transitions\r\n            [ACCEPTING] = {\r\n                [INVALID]   = TRAP,\r\n                [ZERO_CHAR] = TRAP,\r\n                [ONE_CHAR]  = ACCEPTING,\r\n            }\r\n\r\n        }\r\n\r\n    };\r\n```\r\n\r\nWe initialize the current state like so,\r\n\r\n```c\r\n    .curr_state = ACCEPTING,\r\n```\r\n\r\ndesignate the template `map()` function like so,\r\n\r\n```c\r\n    .map = map_chr,\r\n```\r\n\r\nand then initialize the state table using the same initialization syntax as earlier.\r\n\r\n```c\r\n    // States\r\n    .state_table = {\r\n\r\n        // Trap state transitions\r\n        [TRAP] = {\r\n            [INVALID]   = TRAP,\r\n            [ZERO_CHAR] = TRAP,\r\n            [ONE_CHAR]  = TRAP,\r\n        },\r\n\r\n        // Accept state transitions\r\n        [ACCEPTING] = {\r\n            [INVALID]   = TRAP,\r\n            [ZERO_CHAR] = TRAP,\r\n            [ONE_CHAR]  = ACCEPTING,\r\n        }\r\n\r\n    }\r\n```\r\n\r\nReading the rows in the `state_table` in order, we see that when the FSA is in a rejecting state, all three possible inputs (`'0'`, `'1'`, and invalid input) all result in rejection. This is typical for more-formal DFAs (a lot of informal DFAs tend to not show the trap state and it is just implied that an invalid input immediately kills the machine).\r\n\r\nNote another quirk with C initialization of arrays. If you recall the `0` assumption trick mentioned earlier in the section about `char_map`, the same trick can be used here. By denoting the trap (rejecting) state as the first state in the state `enum` declaration, we can assume that as long as there is something initialized in the `state_table`, all undeclared transitions default to the trap state (i.e. instant rejection). In reality, this declaration is unneeded:\r\n\r\n```c\r\n    // Trap state transitions\r\n    [TRAP] = {\r\n        [INVALID]   = TRAP,\r\n        [ZERO_CHAR] = TRAP,\r\n        [ONE_CHAR]  = TRAP,\r\n    },\r\n```\r\n\r\nas because `enum`s start counting from `0` and our `TRAP` (rejecting) state is the first listed in the `state` `enum`, those lines actually are equivalent to:\r\n\r\n```c\r\n    // Trap state transitions\r\n    [0] = {\r\n        [INVALID]   = 0,\r\n        [ZERO_CHAR] = 0,\r\n        [ONE_CHAR]  = 0,\r\n    },\r\n```\r\n\r\nin fact, if we look at the declaration of the `state_table` as a whole:\r\n\r\n```c\r\n    // States\r\n    .state_table = {\r\n\r\n        // Trap state transitions\r\n        [TRAP] = {\r\n            [INVALID]   = TRAP,\r\n            [ZERO_CHAR] = TRAP,\r\n            [ONE_CHAR]  = TRAP,\r\n        },\r\n\r\n        // Accept state transitions\r\n        [ACCEPTING] = {\r\n            [INVALID]   = TRAP,\r\n            [ZERO_CHAR] = TRAP,\r\n            [ONE_CHAR]  = ACCEPTING,\r\n        }\r\n\r\n    }\r\n```\r\n\r\nlooking through the `enum`s, we actually get:\r\n\r\n```c\r\n    .state_table = {\r\n        \r\n        [0] = {\r\n            [0]  = 0,\r\n            [48] = 0,\r\n            [49] = 0,\r\n        },\r\n\r\n        [1] = {\r\n            [0]  = 0,\r\n            [48] = 0,\r\n            [49] = 1,\r\n        }\r\n\r\n    }\r\n```\r\n\r\nwhich reduces to:\r\n\r\n```c\r\n    .state_table = {\r\n        { [0]  = 0, [48] = 0, [49] = 0 },\r\n        { [0]  = 0, [48] = 0, [49] = 1 }\r\n    }\r\n```\r\n\r\nwhich reduces to:\r\n\r\n```c\r\n    .state_table = {\r\n        [1] = {0, 0, 1}\r\n    }\r\n```\r\n\r\nwhich reduces to:\r\n\r\n```c\r\n    .state_table = {\r\n        [1] = {[2] = 1}\r\n    }\r\n```\r\n\r\nor\r\n\r\n```c\r\n    .state_table = {\r\n        [ACCEPTING] = { [ONE_CHAR] = ACCEPTING }\r\n    }\r\n```\r\n\r\nas everything else is mapped to the `TRAP` state, which we assumed to be the C default initializer value of 0. This one-line declaration is equivalent to the gigantic\r\n\r\n```c\r\n    // States\r\n    .state_table = {\r\n\r\n        // Trap state transitions\r\n        [TRAP] = {\r\n            [INVALID]   = TRAP,\r\n            [ZERO_CHAR] = TRAP,\r\n            [ONE_CHAR]  = TRAP,\r\n        },\r\n\r\n        // Accept state transitions\r\n        [ACCEPTING] = {\r\n            [INVALID]   = TRAP,\r\n            [ZERO_CHAR] = TRAP,\r\n            [ONE_CHAR]  = ACCEPTING,\r\n        }\r\n\r\n    }\r\n```\r\n\r\nshown at first, but it is sometimes worth listing every transition for the sake of understanding and clarity. That being said, in some of the code samples in the `examples/` folder, I do not include the trap state. The first subscript (row) is not encoded in the array, but is actually used with the second subscript (column) as a displacement to get to the next state. So, by relying on C's default initialization quirk, we can default the most common transition in DFAs (implied rejection) to `0`. For more in-depth examples of this initialization quirk, look at `valid_number.c` and `date_validator.c`.\r\n\r\nIn actually using the machine, for this particular example one could perform:\r\n\r\n```c\r\nfor (int i = 0; input_string[i] != '\\0'; i++) {\r\n    GET_NEXT_STATE(machine, \u0026input_string[i]);\r\n}\r\n\r\nputs(GET_STATE(machine) == ACCEPTING ? \"Input accepted\" : \"Input rejected\");\r\n```\r\n\r\n## Testing\r\n\r\nIn the `examples/` folder there is a `makefile` you can use to run all the example programs.\r\n\r\nIn creating my example FSAs I create self-checking test harnesses that (usually) rely on test cases of the form:\r\n\r\n```c\r\nstruct test_case {\r\n    char input[16];\r\n    int expected_result;\r\n};\r\n```\r\n\r\nand then with an array of these `test_case`s, testing the stately machines using a test loop of the form:\r\n\r\n```c\r\n    for (int i = 0; i \u003c (int)(sizeof(tests) / sizeof(*tests)); i++) {\r\n        printf(\"Testing case '%s'\\n\", tests[i].input);\r\n        SET_STATE(machine, FIRST_DIGIT);\r\n        for (int c = 0; tests[i].input[c]; c++) {\r\n            (void)GET_NEXT_STATE(machine, \u0026tests[i].input[c]);\r\n        }\r\n        if (GET_STATE(machine) != tests[i].expected_result) {\r\n            puts(\"\");\r\n            printf(\"    Expected %s but got %s\\n\", texts[tests[i].expected_result], texts[GET_STATE(machine)]);\r\n            puts(\"\");\r\n            return 1;\r\n        }\r\n    }\r\n```\r\n\r\nFor a more in-depth gander at this, look at `date_validator.c` (and if you scroll to lines 634-698 you will get a taste of how robust the `TRAP` state / `INVALID` input initialization quirk is).\r\n\r\n## Random Examples\r\n\r\n_An example of creating a `map()` function that uses a `struct` to derive a state_:\r\n\r\n```c\r\nenum input { INVALID, EVEN_INPUT, ODD_INPUT };\r\nenum state { TRAP, START, EVEN_STATE, ODD_STATE };\r\n\r\nstruct request {\r\n    int a;\r\n    int b;\r\n    int c;\r\n};\r\n\r\nint request_to_state(const void *req_ptr) {\r\n    struct request req = *(struct request *)req_ptr;\r\n    return (req.a + req.b + req.c) % 2 ? ODD_INPUT : EVEN_INPUT;\r\n}\r\n```\r\n\r\n_Relying on a more explicit way of declaring `INVALID` input (and not relying on initializaiton quirks)_:\r\n\r\n```c\r\nenum input { INVALID, DIGIT, SCIENTIFIC_E, PLUS_MINUS, PERIOD };\r\n\r\nint map_chr(const void *chr) {\r\n    char c = *(char *)chr;\r\n    if (c \u003e= '0' \u0026\u0026 c \u003c= '9')\r\n        return DIGIT;\r\n    if (c == 'E' || c == 'e')\r\n        return SCIENTIFIC_E;\r\n    if (c == '+' || c == '-')\r\n        return PLUS_MINUS;\r\n    if (c == '.')\r\n        return PERIOD;\r\n    return INVALID;\r\n}\r\n```\r\n\r\n## More Reading\r\n\r\n[Modern-day regex vs DFA regex](https://swtch.com/~rsc/regexp/regexp1.html)\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjnguyen1098%2Fstately","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjnguyen1098%2Fstately","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjnguyen1098%2Fstately/lists"}