{"id":13782863,"url":"https://github.com/ffwff/bfjit","last_synced_at":"2025-10-30T07:02:42.164Z","repository":{"id":105851301,"uuid":"158386292","full_name":"ffwff/bfjit","owner":"ffwff","description":"JIT interpreter for brainfuck","archived":false,"fork":false,"pushed_at":"2018-12-31T17:08:03.000Z","size":23,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-12T22:55:32.962Z","etag":null,"topics":["brainfuck","brainfuck-compiler"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ffwff.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-11-20T12:28:47.000Z","updated_at":"2023-06-21T12:26:46.000Z","dependencies_parsed_at":"2023-06-26T01:14:30.998Z","dependency_job_id":null,"html_url":"https://github.com/ffwff/bfjit","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ffwff/bfjit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ffwff%2Fbfjit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ffwff%2Fbfjit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ffwff%2Fbfjit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ffwff%2Fbfjit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ffwff","download_url":"https://codeload.github.com/ffwff/bfjit/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ffwff%2Fbfjit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281761849,"owners_count":26557114,"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-10-30T02:00:06.501Z","response_time":61,"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":["brainfuck","brainfuck-compiler"],"created_at":"2024-08-03T18:01:46.534Z","updated_at":"2025-10-30T07:02:42.128Z","avatar_url":"https://github.com/ffwff.png","language":"C","funding_links":[],"categories":["C"],"sub_categories":[],"readme":"# bfjit\n\nExperiments I did making JIT brainfuck interpreter targeting the x86-64 architecture.\n\n## Optimizations\n\nThe program parses a brainfuck file and transforms it into an x86-64 function `void _bfjit(uint8_t *tape)`, which is then called. Upon execution, `%rdi` is set to the address of the tape and `%rcx` is set to the starting position (`0x00`). I'll use the GAS/AT\u0026T syntax as assembly notation.\n\n### Exploiting x86 registers\n\nThe original brainfuck compiler was written with wraparound behavior: if the current position of the pointer is 0, and we decrement it, it would become the maximum position of the tape (30000). Doing bounds checking and manual wraparound would be too expensive, even more so when the tape length, 30000, [is not a power of 2](https://stackoverflow.com/questions/11040646/faster-modulus-in-c-c).\n\nWhich was one of the reasons I increased the tape size to 65536, the maximum value of a 16-bit integer. x86-64 actually provides us with several 16-bit general purpose registers we can use (`%ax`, `%cx`, `%dx`, and `%bx`). If we add an integer to one of these registers, and it overflows, it will wraparound back to 0. In `bfjit-64k*.c`, I used this trick and made the `%cx` register store our pointer position.\n\n### Fusing value changes\n\nIf there is a consecutive series of `+` or `-`, we can calculate the summed delta of the series (`+` being 1 and `-` being -1), and add that to the value in tape instead. So a chain of `+++-`, instead of becoming,\n\n```asm\nincb (%rcx,%rdi,1)\nincb (%rcx,%rdi,1)\nincb (%rcx,%rdi,1)\ndecb (%rcx,%rdi,1)\n```\n\nit would become,\n\n```asm\naddb $0x2, (%rcx, %rdi,1)\n```\n\n### Fusing pointer changes\n\nSimilarly, if there is a consecutive series of `\u003e` or `\u003c`, we can calculate the summed delta and add that to the tape position. Instead of `\u003e\u003e\u003e` becoming\n\n```asm\ninc %cx\ninc %cx\ninc %cx\n```\n\nit would become,\n\n```asm\nadd $0x3, %cx\n```\n\n### Postponing movements\n\nIf there is a series of bf commands containing only `+`, `-`, `\u003e`, `\u003c`, we can keep track of pointer movements and add them in one shot once there is a `[`, `]`, `.` or `,` command. So from `\u003e+++\u003e` instead of becoming,\n\n```asm\ninc %cx\naddb $0x3, (%rcx, %rdi, 1)\ninc %cx\n```\n\nwould become\n\n```asm\naddb $0x3, 0x1(%rcx, %rdi, 1)\naddb $0x3, %cx\n```\n\nPostponing movements are implemented in `bfjit-64k-postpone.c`, however it is very buggy (it doesn't run `hanoi.b` correctly), posibbly due to x86 opcodes being a mess.\n\n### Clear loops\n\nIf there is a series of `[-]`, we simply just clear the value in that location in tape:\n\n```asm\nmovb $0x0, (%rcx, %rdi, 1)\n```\n\nIf there is a series of `+` or `-` following it, instead of clearing, we set the value to the summed delta, so `[-]+++` becomes\n\n```asm\nmovb $0x3, (%rcx, %rdi, 1)\n```\n\nSeveral programs chain clear loops, we can make these chained clears faster by bitmasking the 64-bit value starting from the current position in tape with a calculated mask. So `[-]\u003e[-]\u003e[-]` becomes\n\n```asm\nmov $0x0, (%rcx, %rdi, 1)\nand $0xffff0000, (%rcx, %rdi, 1)\n```\n\n### Simple loops\n\nIf the loop body has no subloops and no input/output, all the movements add up to 0, and all the increments/decrements at the current location in tape add up to −1, we can remove the loop jumps and condition checks, and transform all value changes inside the loop to be a multiple of the value at the current location in tape.\n\n\n## Implementation notes\n\n* Although the interpreter allows the tape pointer to be wrapped around, one can easily place a \"simple loop\" or chained clear loops to overflow the buffer, This can be fixed by doing proper bounds checking (which would defeat the point of these optimizations) or using [funky memory maps](https://nullprogram.com/blog/2016/04/10/) and limiting optimization pointer movements.\n* This interpreter doesn't use an IR to translate, it directly translates bf to x86-64 opcodes. Using an IR would tremendously help development time and open up to new architecture possibilites. However, i'm too lazy.\n\n### References\n\n* [Optimizing brainfuck compiler](https://www.nayuki.io/page/optimizing-brainfuck-compiler)\n* [An optimising brainfuck compiler](http://www.wilfred.me.uk/blog/2015/08/29/an-optimising-bf-compiler/)\n* [Optimzing brainfuck](http://calmerthanyouare.org/2015/01/07/optimizing-brainfuck.html)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fffwff%2Fbfjit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fffwff%2Fbfjit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fffwff%2Fbfjit/lists"}