├── .github ├── FUNDING.yml ├── build ├── run-tests.sh └── workflows │ ├── pull_request.yml │ ├── push.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── compiler ├── compiler.go ├── compiler_test.go ├── generator.go └── generator_test.go ├── go.mod ├── instructions └── instructions.go ├── lexer ├── lexer.go └── lexer_test.go ├── main.go ├── test.sh └── token ├── token.go └── token_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: skx 3 | custom: https://steve.fi/donate/ 4 | -------------------------------------------------------------------------------- /.github/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Build releases for Linux/AMD64 4 | # 5 | # Since we output assembly language there's little point building for 6 | # other systems. 7 | # 8 | 9 | 10 | export GOOS=linux 11 | export GOARCH=amd64 12 | 13 | go build -o "math-compiler-linux-amd64" 14 | -------------------------------------------------------------------------------- /.github/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # I don't even .. 4 | go env -w GOFLAGS="-buildvcs=false" 5 | 6 | # Install the tools we use to test our code-quality. 7 | # 8 | # Here we setup the tools to install only if the "CI" environmental variable 9 | # is not empty. This is because locally I have them installed. 10 | # 11 | # NOTE: Github Actions always set CI=true 12 | # 13 | if [ -n "${CI}" ] ; then 14 | go install golang.org/x/lint/golint@latest 15 | go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest 16 | go install honnef.co/go/tools/cmd/staticcheck@latest 17 | fi 18 | 19 | 20 | # Run the static-check tool. 21 | t=$(mktemp) 22 | staticcheck -checks all ./... > "$t" 23 | if [ -s "$t" ]; then 24 | echo "Found errors via 'staticcheck'" 25 | cat "$t" 26 | rm "$t" 27 | exit 1 28 | fi 29 | rm "$t" 30 | 31 | # At this point failures cause aborts 32 | set -e 33 | 34 | # Run the linter 35 | echo "Launching linter .." 36 | golint -set_exit_status ./... 37 | echo "Completed linter .." 38 | 39 | # Run the shadow-checker 40 | echo "Launching shadowed-variable check .." 41 | go vet -vettool="$(which shadow)" ./... 42 | echo "Completed shadowed-variable check .." 43 | 44 | # Run golang tests 45 | go test ./... 46 | 47 | # Run our functionality tests 48 | ./test.sh 49 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | name: Pull Request 3 | jobs: 4 | test: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Test 10 | uses: skx/github-action-tester@master 11 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: Push Event 6 | jobs: 7 | test: 8 | name: Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Test 13 | uses: skx/github-action-tester@master 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: release 2 | name: Handle Release 3 | jobs: 4 | upload: 5 | name: Upload 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Upload 10 | uses: skx/github-action-publish-binaries@master 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | with: 14 | args: math-compiler-* 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.s 2 | a.out 3 | math-compiler 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/skx/math-compiler)](https://goreportcard.com/report/github.com/skx/math-compiler) 2 | [![license](https://img.shields.io/github/license/skx/math-compiler.svg)](https://github.com/skx/math-compiler/blob/master/LICENSE) 3 | 4 | Table of Contents 5 | ================= 6 | 7 | * [math-compiler](#math-compiler) 8 | * [Installation](#installation) 9 | * [Quick Overview](#quick-overview) 10 | * [About Our Output](#about-our-output) 11 | * [Test Cases](#test-cases) 12 | * [Debugging the generated programs](#debugging-the-generated-programs) 13 | * [Possible Expansion?](#possible-expansion) 14 | * [See Also](#see-also) 15 | * [Github Setup](#github-setup) 16 | 17 | 18 | 19 | 20 | # math-compiler 21 | 22 | This project contains the simplest possible compiler, which converts mathematical operations into assembly language, allowing all the speed in your sums! 23 | 24 | Because this is a simple project it provides only a small number of primitives: 25 | 26 | * `+` - Plus 27 | * `-` - Minus 28 | * `*` - Multiply 29 | * `/` - Divide 30 | * `^` - Raise to a power 31 | * `%` - Modulus 32 | * `!` - Factorial 33 | * `abs` 34 | * `sin` 35 | * `cos` 36 | * `tan` 37 | * `sqrt` 38 | * Stack operations: 39 | * `swap` - Swap the top-two items on the stack 40 | * `dup` - Duplicate the topmost stack-entry. 41 | * Built-in constants: 42 | * `e` 43 | * `pi` 44 | 45 | Despite this toy-functionality there is a lot going on, and we support: 46 | 47 | * Full RPN input 48 | * Floating-point numbers (i.e. one-third multipled by nine is 3) 49 | * `1 3 / 9 *` 50 | * Negative numbers work as you'd expect. 51 | 52 | Some errors will be caught at run-time, as the generated code has support for: 53 | 54 | * Detecting, and preventing, division by zero. 55 | * Detecting insufficient arguments being present upon the stack. 56 | * For example this program is invalid `3 +`, because the addition operator requires two operands. (i.e. `3 4 +`) 57 | 58 | 59 | 60 | ## Installation 61 | 62 | If you just need a binary you can find them upon the [project release page](https://github.com/skx/math-compiler/releases), however if you wish to build and install locally you can do that in either of the standard ways: 63 | 64 | 1. Install from the latest revision: 65 | 66 | ```sh 67 | $ go install github.com/skx/math-compiler@master 68 | ``` 69 | 70 | 2. Or you can clone the source, and build from it: 71 | 72 | ```sh 73 | $ git clone https://github.com/skx/math-compiler 74 | $ cd math-compiler 75 | $ go install . 76 | ``` 77 | 78 | 79 | 80 | ## Quick Overview 81 | 82 | The intention of this project is mostly to say "I wrote a compiler", because I've already [experimented with a language](https://github.com/skx/monkey/), an [embedded evaluation engine](https://github.com/skx/evalfilter/), and [implemented a BASIC interpreter](https://github.com/skx/gobasic/). The things learned from those projects were pretty useful, even if the actual results were not so obviously useful in themselves. 83 | 84 | Because there are no shortages of toy-languages, and there is a lot of complexity in writing another for no real gain, I decided to just focus upon a simple core: 85 | 86 | * Allowing "maths stuff" to be "compiled". 87 | 88 | In theory this would allow me to compile things like this: 89 | 90 | 2 + ( 4 * 54 ) 91 | 92 | However I even simplified that, via the use of a "[Reverse Polish](https://en.wikipedia.org/wiki/Reverse_Polish_notation)" notation, so if you want to run that example you'd enter the expression as: 93 | 94 | 4 54 * 2 + 95 | 96 | 97 | 98 | ## About Our Output 99 | 100 | The output of `math-compiler` will be an assembly-language file, which then needs to be compiled before it may be executed. 101 | 102 | Given our previous example of `2 + ( 4 * 54)` we can compile & execute that program like so: 103 | 104 | $ math-compiler '4 54 * 2+' > sample.s 105 | $ gcc -static -o sample ./sample.s 106 | $ ./sample 107 | Result 218 108 | 109 | There you see: 110 | 111 | * `math-compiler` was invoked, and the output written to the file `sample.s`. 112 | * `gcc` was used to assemble `sample.s` into the binary `sample`. 113 | * The actual binary was then executed, which showed the result of the calculation. 114 | 115 | If you prefer you can also let the compiler do the heavy-lifting, and generate an executable for you directly. Simply add `-compile`, and execute the generated `a.out` binary: 116 | 117 | $ math-compiler -compile=true '2 8 ^' 118 | $ ./a.out 119 | Result 256 120 | 121 | Or to compile __and__ execute directly: 122 | 123 | $ math-compiler -run '3 45 * 9 + 12 /' 124 | Result 12 125 | 126 | 127 | 128 | ## Test Cases 129 | 130 | The codebase itself contains some simple test-cases, however these are not comprehensive as a large part of our operation is merely to populate a simple template-file, and it is hard to test that. 131 | 132 | To execute the integrated tests use the standard go approach: 133 | 134 | $ go test [-race] ./... 135 | 136 | In addition to the internal test cases there are also some functional tests 137 | contained in [test.sh](test.sh) - these perform some calculations and verify 138 | they produce the correct result. 139 | 140 | frodo ~/go/src/github.com/skx/math-compiler $ ./test.sh 141 | ... 142 | Expected output found for '2 0 ^' [0] 143 | Expected output found for '2 1 ^' [2] 144 | Expected output found for '2 2 ^' [4] 145 | Expected output found for '2 3 ^' [8] 146 | Expected output found for '2 4 ^' [16] 147 | Expected output found for '2 5 ^' [32] 148 | ... 149 | Expected output found for '2 30 ^' [1073741824] 150 | ... 151 | 152 | 153 | ### Debugging the generated programs 154 | 155 | If you run the compiler with the `-debug` flag a breakpoint will be generated 156 | immediately at the start of the program. You can use that breakpoint to easily 157 | debug the generated binary via `gdb`. 158 | 159 | For example you might generate a program "`2 3 + 4 /`" like so: 160 | 161 | $ math-compiler -compile -debug '2 3 + 4 /' 162 | 163 | Now you can launch that binary under `gdb`, and run it: 164 | 165 | $ gdb ./a.out 166 | (gdb) run 167 | .. 168 | Program received signal SIGTRAP, Trace/breakpoint trap. 169 | 0x00000000006b20cd in main () 170 | 171 | Dissassemble the code via `disassemble`, and step over instructions one at a time via `stepi`. If your program is long you might see a lot of output from the `disassemble` step: 172 | 173 | (gdb) disassemble 174 | Dump of assembler code for function main: 175 | 0x00000000006b20cb: push %rbp 176 | 0x00000000006b20cc: int3 177 | => 0x00000000006b20cd: fldl 0x6b20b3 178 | 0x00000000006b20d4: fstpl 0x6b2090 179 | 0x00000000006b20db: mov 0x6b2090,%rax 180 | 0x00000000006b20e3: push %rax 181 | 0x00000000006b20e4: fldl 0x6b20bb 182 | 0x00000000006b20eb: fstpl 0x6b2090 183 | 0x00000000006b20f2: mov 0x6b2090,%rax 184 | 0x00000000006b20fa: push %rax 185 | ... 186 | ... 187 | 188 | You can set a breakpoint at a line in the future, and continue running till 189 | you hit it, with something like this: 190 | 191 | (gdb) break *0x00000000006b20fa 192 | (gdb) cont 193 | 194 | Once there inspect the registers with commands like these two: 195 | 196 | (gdb) print $rax 197 | (gdb) info registers 198 | 199 | My favourite is `info registers float`, which shows you the floating-point 200 | values as well as the raw values: 201 | 202 | (gdb) info registers float 203 | st0 0.140652076786443369638 (raw 0x3ffc90071917a6263000) 204 | st1 0 (raw 0x00000000000000000000) 205 | st2 0 (raw 0x00000000000000000000) 206 | ... 207 | ... 208 | 209 | Further documentation can be found in the `gdb` manual, which is worth reading 210 | if you've an interest in compilers, debuggers, and decompilers. 211 | 212 | 213 | 214 | ## Possible Expansion? 215 | 216 | The obvious thing to improve in this compiler is to add support for more operations. At the moment support for the most obvious/common operations is present, but perhaps more functions could be added. 217 | 218 | 219 | 220 | ## See Also 221 | 222 | If you enjoyed this repository, then you might also enjoy my compiler for the [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) language. The compiler there compiles brainfuck programs to x86-64 assembly-language: 223 | 224 | * [https://github.com/skx/bfcc](https://github.com/skx/bfcc) 225 | 226 | 227 | 228 | ## Github Setup 229 | 230 | This repository is configured to run tests upon every commit, and when 231 | pull-requests are created/updated. The testing is carried out via 232 | [.github/run-tests.sh](.github/run-tests.sh) which is used by the 233 | [github-action-tester](https://github.com/skx/github-action-tester) action. 234 | 235 | Releases are automated in a similar fashion via [.github/build](.github/build), 236 | and the [github-action-publish-binaries](https://github.com/skx/github-action-publish-binaries) action. 237 | 238 | 239 | Steve 240 | -- 241 | -------------------------------------------------------------------------------- /compiler/compiler.go: -------------------------------------------------------------------------------- 1 | // Package compiler contains the core of our compiler. 2 | // 3 | // In brief we go through a three-step process: 4 | // 5 | // 1. We use the lexer to tokenize the expression. 6 | // 7 | // 2. We convert our series of tokens to an internal form. 8 | // 9 | // 3. We walk our internal form, generating output for each block. 10 | // 11 | // There are only one minor complication - storing all the input-floats 12 | // in the data-area of the program. These require escaping for uniqueness 13 | // purposes, and to avoid issues with the assembling. 14 | // 15 | // That said this is a toy, and will remain a toy, so I can live with 16 | // these problems. 17 | // 18 | package compiler 19 | 20 | import ( 21 | "fmt" 22 | "math" 23 | 24 | "github.com/skx/math-compiler/instructions" 25 | "github.com/skx/math-compiler/lexer" 26 | "github.com/skx/math-compiler/token" 27 | ) 28 | 29 | // Compiler holds our object-state. 30 | type Compiler struct { 31 | 32 | // debug holds a flag to decide if debugging "stuff" is generated 33 | // in the output assembly. 34 | debug bool 35 | 36 | // expression holds the mathematical expression we're compiling. 37 | expression string 38 | 39 | // 40 | // Constants we come across. 41 | // 42 | // Rather than placing the numbers in-line we're storing them 43 | // in the constant area. This gives us a level of indirection, 44 | // but it simpler to reason about. 45 | // 46 | // Note that if we see "3 + 4 + 3 + 4" we only store each of 47 | // the constants once each. So we have a map here, key is the 48 | // value of the constant, and `bool` to record that we found it. 49 | // 50 | // Later we'll output just the unique keys. 51 | // 52 | constants map[string]bool 53 | 54 | // tokens holds the expression, broken down into a series of tokens. 55 | // 56 | // The tokens are received from the lexer, and are not modified. 57 | tokens []token.Token 58 | 59 | // Instructions is the virtual instructions we're going to compile 60 | // to assembly 61 | instructions []instructions.Instruction 62 | } 63 | 64 | // 65 | // Our public API consists of the three functions: 66 | // New 67 | // SetDebug 68 | // Compile 69 | // 70 | // The rest of the code is an implementation detail. 71 | // 72 | 73 | // New creates a new compiler, given the expression in the constructor. 74 | func New(input string) *Compiler { 75 | c := &Compiler{expression: input, constants: make(map[string]bool), debug: false} 76 | return c 77 | } 78 | 79 | // SetDebug changes the debug-flag for our output. 80 | func (c *Compiler) SetDebug(val bool) { 81 | c.debug = val 82 | } 83 | 84 | // Compile converts the input program into a collection of 85 | // AMD64-assembly language. 86 | func (c *Compiler) Compile() (string, error) { 87 | 88 | // 89 | // Parse the program into a series of statements, etc. 90 | // 91 | // At this point there might be errors. If so report them, 92 | // and terminate. 93 | // 94 | err := c.tokenize() 95 | if err != nil { 96 | return "", err 97 | } 98 | 99 | // 100 | // Convert the parsed-tokens to in internal-form. 101 | // 102 | c.makeinternalform() 103 | 104 | // 105 | // Now generate the output assembly 106 | // 107 | out := c.output() 108 | 109 | return out, nil 110 | } 111 | 112 | // tokenize populates our internal list of tokens, as a result of 113 | // lexing the input string. 114 | // 115 | // There is some error-handling to ensure that the program looks 116 | // somewhat reasonable. 117 | func (c *Compiler) tokenize() error { 118 | 119 | // 120 | // Create the lexer, which will parse our expression. 121 | // 122 | lexed := lexer.New(c.expression) 123 | 124 | // 125 | // First of all populate that `program` array with our tokens. 126 | // 127 | for { 128 | // Get the next token. 129 | tok := lexed.NextToken() 130 | 131 | // If end of stream then break. 132 | if tok.Type == token.EOF { 133 | break 134 | } 135 | 136 | // If error then abort. 137 | if tok.Type == token.ERROR { 138 | return (fmt.Errorf("error parsing input; token.ERROR returned from the lexer: %s", tok.Literal)) 139 | } 140 | 141 | // 142 | // We'll convert "pi" and "e" into numbers as a special case. 143 | // 144 | if tok.Type == token.PI { 145 | tok.Type = token.NUMBER 146 | tok.Literal = fmt.Sprintf("%f", math.Pi) 147 | } 148 | if tok.Type == token.E { 149 | tok.Type = token.NUMBER 150 | tok.Literal = fmt.Sprintf("%f", math.E) 151 | } 152 | 153 | // Otherwise append the token to our program. 154 | c.tokens = append(c.tokens, tok) 155 | } 156 | 157 | // 158 | // If the program is empty that's an error. 159 | // 160 | if len(c.tokens) < 1 { 161 | return (fmt.Errorf("the input expression was empty")) 162 | } 163 | 164 | // 165 | // If the first token isn't a number we're in trouble 166 | // 167 | if c.tokens[0].Type != token.NUMBER { 168 | return (fmt.Errorf("we expected the program to begin with a numeric thing")) 169 | } 170 | 171 | // 172 | // Get the last token 173 | // 174 | if len(c.tokens) > 1 { 175 | len := len(c.tokens) 176 | end := c.tokens[len-1] 177 | if end.Type == token.NUMBER { 178 | return fmt.Errorf("program ends with a number, which is invalid") 179 | } 180 | } 181 | 182 | // 183 | // No error. 184 | // 185 | return nil 186 | } 187 | 188 | // makeinternalform converts our series of tokens (i.e. the lexed expression) 189 | // into an an intermediary form, collecting constants as they are discovered. 190 | // 191 | // This is the middle-step before generating our assembly-language program. 192 | func (c *Compiler) makeinternalform() { 193 | 194 | // 195 | // Walk our tokens. 196 | // 197 | for _, t := range c.tokens { 198 | 199 | // 200 | // Handle each kind. 201 | // 202 | switch t.Type { 203 | 204 | case token.ABS: 205 | 206 | c.instructions = append(c.instructions, 207 | instructions.Instruction{Type: instructions.Abs}) 208 | 209 | case token.ASTERISK: 210 | 211 | c.instructions = append(c.instructions, 212 | instructions.Instruction{Type: instructions.Multiply}) 213 | 214 | case token.FACTORIAL: 215 | 216 | c.instructions = append(c.instructions, 217 | instructions.Instruction{Type: instructions.Factorial}) 218 | 219 | case token.COS: 220 | 221 | c.instructions = append(c.instructions, 222 | instructions.Instruction{Type: instructions.Cos}) 223 | 224 | case token.DUP: 225 | 226 | c.instructions = append(c.instructions, 227 | instructions.Instruction{Type: instructions.Dup}) 228 | 229 | case token.NUMBER: 230 | 231 | // Mark the constant as having been used. 232 | c.constants[t.Literal] = true 233 | 234 | // add the instruction 235 | c.instructions = append(c.instructions, 236 | instructions.Instruction{Type: instructions.Push, Value: t.Literal}) 237 | 238 | case token.MOD: 239 | 240 | c.instructions = append(c.instructions, 241 | instructions.Instruction{Type: instructions.Modulus}) 242 | 243 | case token.MINUS: 244 | 245 | c.instructions = append(c.instructions, 246 | instructions.Instruction{Type: instructions.Minus}) 247 | 248 | case token.PLUS: 249 | 250 | c.instructions = append(c.instructions, 251 | instructions.Instruction{Type: instructions.Plus}) 252 | 253 | case token.POWER: 254 | 255 | // add the instruction 256 | c.instructions = append(c.instructions, 257 | instructions.Instruction{Type: instructions.Power}) 258 | 259 | case token.SIN: 260 | 261 | c.instructions = append(c.instructions, 262 | instructions.Instruction{Type: instructions.Sin}) 263 | 264 | case token.SLASH: 265 | 266 | c.instructions = append(c.instructions, 267 | instructions.Instruction{Type: instructions.Divide}) 268 | 269 | case token.SQRT: 270 | 271 | c.instructions = append(c.instructions, 272 | instructions.Instruction{Type: instructions.Sqrt}) 273 | 274 | case token.SWAP: 275 | 276 | c.instructions = append(c.instructions, 277 | instructions.Instruction{Type: instructions.Swap}) 278 | 279 | case token.TAN: 280 | 281 | c.instructions = append(c.instructions, 282 | instructions.Instruction{Type: instructions.Tan}) 283 | 284 | } 285 | } 286 | 287 | } 288 | 289 | // output generates the output, joining a header, a footer, and the 290 | // writes our program to stdout 291 | func (c *Compiler) output() string { 292 | 293 | // 294 | // The header. 295 | // 296 | header := ` 297 | # 298 | # This assembly file was created by math-compiler. 299 | # 300 | # We're going to use Intel Syntax, because that is what I grew up with. 301 | # 302 | .intel_syntax noprefix 303 | .global main 304 | 305 | # Data-section: 306 | # 307 | # This contains data for the program at run-time. 308 | # 309 | # int: is used to push values onto the stack. 310 | # 311 | # a: used as an argument for functions that require one/two operands. 312 | # 313 | # b: used as an argument for functions that require two operands. 314 | # 315 | # depth: used to keep track of stack-depth. 316 | # 317 | # fmt: Used to output the result of the calculation, later strings are for 318 | # various error-reports. 319 | # 320 | .data 321 | a: .double 0.0 322 | b: .double 0.0 323 | depth: .double 0.0 324 | int: .double 0.0 325 | 326 | fmt: .asciz "Result %g\n" 327 | div_zero: .asciz "Attempted division by zero. Aborting\n" 328 | overflow: .asciz "Overflow - value out of range. Aborting\n" 329 | stack_err: .asciz "Insufficient entries on the stack. Aborting\n" 330 | stack_full: .asciz "Too many entries remaining on the stack. Aborting\n" 331 | ` 332 | 333 | // 334 | // Output each of our discovered constants. 335 | // 336 | for v := range c.constants { 337 | header += fmt.Sprintf("%s: .double %s\n", 338 | c.escapeConstant(v), v) 339 | } 340 | 341 | header += ` 342 | # 343 | # Main is our entry-point. 344 | # 345 | # We'll save rbp before we begin 346 | # 347 | main: 348 | push rbp 349 | 350 | # Our stack is initially empty (of numbers), so ensure that [depth] 351 | # is set to zero. 352 | # 353 | # Every time we push a value upon the stack we'll increase this value 354 | # and before we pop arguments from the stack we'll check there are 355 | # sufficient values stored. This will prevent segfaults when user 356 | # programs are broken. 357 | # 358 | mov qword ptr [depth], 0 359 | 360 | ` 361 | if c.debug { 362 | header += " # Debug-break\n" 363 | header += " int 03\n" 364 | } 365 | 366 | // 367 | // The body of the program 368 | // 369 | body := "" 370 | 371 | // Now we walk over our internal-representation, and output 372 | // a chunk of assembly for each of our operator-types. 373 | for i, opr := range c.instructions { 374 | 375 | // 376 | // One-handler for each type: Alphabetical order. 377 | // 378 | switch opr.Type { 379 | 380 | case instructions.Abs: 381 | body += c.genAbs() 382 | 383 | case instructions.Cos: 384 | body += c.genCos() 385 | 386 | case instructions.Divide: 387 | body += c.genDivide() 388 | 389 | case instructions.Dup: 390 | body += c.genDup() 391 | 392 | case instructions.Factorial: 393 | body += c.genFactorial(i) 394 | 395 | case instructions.Minus: 396 | body += c.genMinus() 397 | 398 | case instructions.Modulus: 399 | body += c.genModulus() 400 | 401 | case instructions.Multiply: 402 | body += c.genMultiply() 403 | 404 | case instructions.Plus: 405 | body += c.genPlus() 406 | 407 | case instructions.Power: 408 | body += c.genPower(i) 409 | 410 | case instructions.Push: 411 | body += c.genPush(opr.Value) 412 | 413 | case instructions.Sin: 414 | body += c.genSin() 415 | 416 | case instructions.Sqrt: 417 | body += c.genSqrt() 418 | 419 | case instructions.Swap: 420 | body += c.genSwap() 421 | 422 | case instructions.Tan: 423 | body += c.genTan() 424 | 425 | } 426 | } 427 | 428 | footer := ` 429 | # [PRINT] 430 | # ensure there is only one remaining argument upon the stack 431 | mov rax, qword ptr [depth] 432 | cmp rax, 1 433 | jne stack_too_full # should be only one entry. 434 | # print the result 435 | pop rax 436 | mov qword ptr [a], rax 437 | lea rdi,fmt # format string 438 | movq xmm0, [a] # argument 439 | movq rax, 1 # argument count 440 | call printf 441 | pop rbp 442 | xor rax,rax 443 | ret 444 | 445 | 446 | # 447 | # This is hit when a division by zero is attempted. 448 | # 449 | division_by_zero: 450 | lea rdi,div_zero 451 | jmp print_msg_and_exit 452 | 453 | # 454 | # This is hit when a register is too small to hold a value. 455 | # 456 | register_overflow: 457 | lea rdi,overflow 458 | jmp print_msg_and_exit 459 | 460 | 461 | # 462 | # This point is hit when the program is due to terminate, but the 463 | # stack has too many entries upon it. 464 | # 465 | stack_too_full: 466 | lea rdi,stack_full 467 | jmp print_msg_and_exit 468 | 469 | # 470 | # 471 | # This point is hit when there are insufficient operands upon the stack for 472 | # a given operation. (For example '3 +', or '3 4 + /'.) 473 | # 474 | stack_error: 475 | lea rdi,stack_err 476 | # jmp print_msg_and_exit - JMP is unnecessary here. 477 | 478 | # 479 | # Print a message and terminate. 480 | # 481 | # NOTE: We call 'exit' here to allow stdout to be flushed, and also to ensure 482 | # we don't need to balance our stack. 483 | # 484 | print_msg_and_exit: 485 | xor rax,rax 486 | call printf 487 | mov rdi,0 488 | call exit 489 | 490 | ` 491 | 492 | return header + body + footer 493 | } 494 | -------------------------------------------------------------------------------- /compiler/compiler_test.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | // We try to compile several bogus programs 9 | func TestBogusInput(t *testing.T) { 10 | 11 | tests := []string{ 12 | 13 | // empty program 14 | "", 15 | 16 | // program that doesn't start with an int 17 | "+", 18 | 19 | // program with invalid token 20 | "3 5 $", 21 | 22 | // program with a missing operator 23 | "3 3", 24 | 25 | // Again 26 | "3 4 + 3", 27 | } 28 | 29 | for _, test := range tests { 30 | c := New(test) 31 | err := c.tokenize() 32 | if err == nil { 33 | t.Errorf("We expected an error handling '%s', but got none!", test) 34 | } 35 | } 36 | } 37 | 38 | // Test some valid programs. 39 | func TestValidPrograms(t *testing.T) { 40 | 41 | tests := []string{ 42 | "1 2 -", 43 | "3 4 +", 44 | "5 7 *", 45 | "9 3 /", 46 | "10 5 %", 47 | "2 8 ^", 48 | "3 sin", 49 | "4 cos", 50 | "5 tan", 51 | "10 sqrt", 52 | "10 dup +", 53 | "10 3 swap -", 54 | "pi pi *", 55 | "e pi *", 56 | "-2 abs", 57 | } 58 | 59 | for _, test := range tests { 60 | 61 | c := New(test) 62 | 63 | // tokenize 64 | err := c.tokenize() 65 | if err != nil { 66 | t.Errorf("We didn't expect an error tokenizing a valid program, but found one %s", err.Error()) 67 | } 68 | 69 | // convert to internal form 70 | c.makeinternalform() 71 | 72 | // output the text 73 | _ = c.output() 74 | } 75 | } 76 | 77 | // Test some valid programs, with the shortcut 78 | func TestValidProgramsShortcut(t *testing.T) { 79 | 80 | tests := []string{ 81 | "1 2 -", 82 | "3 4 +", 83 | "5 7 *", 84 | "9 3 /", 85 | "10 5 %", 86 | "2 8 ^", 87 | "3 sin", 88 | "4 cos", 89 | "5 tan", 90 | "10 sqrt", 91 | "10 dup +", 92 | "10 3 swap -", 93 | "pi pi *", 94 | "e pi *", 95 | "-2 abs", 96 | } 97 | 98 | for _, test := range tests { 99 | 100 | c := New(test) 101 | _, err := c.Compile() 102 | if err != nil { 103 | t.Errorf("Unexpected error compiling program: %s", err.Error()) 104 | } 105 | } 106 | } 107 | 108 | // Test actually outputing some valid programs. 109 | // 110 | // This test covers the full range: 111 | // "parse". 112 | // "compile". 113 | // "output". 114 | // 115 | // However it doesn't test that the generated output contains what we 116 | // expect. The only way to do that would be to have a static-file and 117 | // compare it literally. If we did that we'd have a pain keeping it 118 | // in sync. 119 | // 120 | // So here we're just looking for rough-behaviour. Sorry! 121 | // 122 | func TestValidOutput(t *testing.T) { 123 | 124 | tests := []string{ 125 | "1 2 -", 126 | "3 4 +", 127 | "5 7 *", 128 | "9 3 /", 129 | "10 5 %", 130 | "2 8 ^", 131 | "2 0 ^", // N ^ 0 is a special case 132 | "2 1 ^", // N ^ 1 is a special case 133 | "2 12 ^", // N ^ 12 is NOT a special case! 134 | } 135 | 136 | for _, test := range tests { 137 | 138 | // create 139 | c := New(test) 140 | 141 | // compile 142 | err := c.tokenize() 143 | if err != nil { 144 | t.Errorf("We didn't expect an error compiling a valid program, but found one %s", err.Error()) 145 | } 146 | 147 | // output 148 | out := c.output() 149 | 150 | // sanity-check 151 | if !strings.Contains(out, "main") { 152 | t.Errorf("Our generated program looked .. bogus?") 153 | } 154 | } 155 | } 156 | 157 | // Test that enabling the debug-flag generates a trap instruction. 158 | func TestDebug(t *testing.T) { 159 | 160 | test := `1 1 +` 161 | 162 | c := New(test) 163 | c.SetDebug(true) 164 | 165 | // tokenize 166 | err := c.tokenize() 167 | if err != nil { 168 | t.Errorf("We didn't expect an error tokenizing a valid program, but found one %s", err.Error()) 169 | } 170 | 171 | // convert to internal form 172 | c.makeinternalform() 173 | 174 | // output the text 175 | out := c.output() 176 | if err != nil { 177 | t.Errorf("We didn't expect an error generating our assembly %s", err.Error()) 178 | } 179 | 180 | if !strings.Contains(out, "int 03") { 181 | t.Errorf("Debug trap not found!") 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /compiler/generator.go: -------------------------------------------------------------------------------- 1 | // generator.go contains the code for emitting instructions. 2 | 3 | package compiler 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // escapeConstant converts a floating-point number such as 12 | // "1.2", or "-1.3" into a constant value that can be embedded 13 | // safely into our generated assembly-language file. 14 | func (c *Compiler) escapeConstant(input string) string { 15 | 16 | // Convert "3.0" to "const_3.0" 17 | s, _ := strconv.ParseFloat(input, 32) 18 | 19 | var val string 20 | if s < 0 { 21 | val = fmt.Sprintf("const_neg_%s", input) 22 | } else { 23 | val = fmt.Sprintf("const_%s", input) 24 | } 25 | 26 | // remove periods 27 | val = strings.Replace(val, ".", "_", -1) 28 | // remove minus-signs 29 | val = strings.Replace(val, "-", "", -1) 30 | return val 31 | } 32 | 33 | // genAbs generates assembly code to pop a value from the stack, 34 | // run an ABS-operation, and store the result back on the stack. 35 | func (c *Compiler) genAbs() string { 36 | return ` 37 | # [ABS] 38 | # ensure there is at least one argument on the stack 39 | mov rax, qword ptr [depth] 40 | cmp rax, 1 41 | jb stack_error 42 | 43 | # pop one value 44 | pop rax 45 | mov qword ptr [a], rax 46 | 47 | # abs 48 | fld qword ptr [a] 49 | fabs 50 | fstp qword ptr [a] 51 | 52 | # push result onto stack 53 | mov rax, qword ptr [a] 54 | push rax 55 | 56 | # stack size didn't change; popped one, pushed one. 57 | ` 58 | } 59 | 60 | // genCos generates assembly code to pop a value from the stack, 61 | // run a cos-operation, and store the result back on the stack. 62 | func (c *Compiler) genCos() string { 63 | return ` 64 | # [COS] 65 | # ensure there is at least one argument on the stack 66 | mov rax, qword ptr [depth] 67 | cmp rax, 1 68 | jb stack_error 69 | 70 | # pop one value 71 | pop rax 72 | mov qword ptr [a], rax 73 | 74 | # cos 75 | fld qword ptr [a] 76 | fcos 77 | fstp qword ptr [a] 78 | 79 | # push result onto stack 80 | mov rax, qword ptr [a] 81 | push rax 82 | 83 | # stack size didn't change; popped one, pushed one. 84 | ` 85 | } 86 | 87 | // genDivide generates assembly code to pop two values from the stack, 88 | // divide them and store the result back on the stack. 89 | func (c *Compiler) genDivide() string { 90 | return ` 91 | # [DIVIDE] 92 | # ensure there are at least two arguments on the stack 93 | mov rax, qword ptr [depth] 94 | cmp rax, 2 95 | jb stack_error 96 | 97 | # pop two values 98 | pop rax 99 | cmp rax,0 100 | je division_by_zero 101 | mov qword ptr [a], rax 102 | pop rax 103 | mov qword ptr [b], rax 104 | 105 | # divide 106 | fld qword ptr [b] 107 | fdiv qword ptr [a] 108 | fstp qword ptr [a] 109 | 110 | # push the result back onto the stack 111 | mov rax, qword ptr [a] 112 | push rax 113 | 114 | # we took two values from the stack, but added one 115 | # so the net result is the stack shrunk by one. 116 | dec qword ptr [depth] 117 | ` 118 | 119 | } 120 | 121 | // genDup generates assembly code to pop a value from the stack and 122 | // push it back twice - effectively duplicating it. 123 | func (c *Compiler) genDup() string { 124 | return ` 125 | # [DUP] 126 | # ensure there is at least one argument on the stack 127 | mov rax, qword ptr [depth] 128 | cmp rax, 1 129 | jb stack_error 130 | 131 | pop rax 132 | push rax 133 | push rax 134 | 135 | # We've added a new entry to the stack. 136 | inc qword ptr [depth] 137 | ` 138 | } 139 | 140 | // genFactorial generates assembly code to pop a value from the stack, 141 | // run a factorial-operation, and store the result back on the stack. 142 | func (c *Compiler) genFactorial(i int) string { 143 | text := ` 144 | # [FACTORIAL] 145 | # ensure there is at least one argument on the stack 146 | mov rax, qword ptr [depth] 147 | cmp rax, 1 148 | jb stack_error 149 | 150 | # pop a value - rounding to an int 151 | pop rax 152 | mov qword ptr [a], rax 153 | fld qword ptr [a] 154 | frndint 155 | fistp qword ptr [a] 156 | 157 | # get the value in rcx, setup rax to be 1 158 | mov rcx, qword ptr [a] 159 | mov rax,1 160 | 161 | # If the value is negative, return zero 162 | cmp rcx, 0 163 | jg again_#ID 164 | # jg means jump-if-greater, so if we hit this we had zero/negative 165 | # store the result. 166 | mov qword ptr[a], 0 167 | jmp store_result_#ID 168 | 169 | again_#ID: 170 | # rax = rax * rcx 171 | imul rax, rcx 172 | 173 | # value too big? 174 | jo register_overflow 175 | 176 | dec rcx 177 | jnz again_#ID 178 | 179 | # store 180 | mov qword ptr[a], rax 181 | fild qword ptr [a] 182 | fstp qword ptr [a] 183 | mov rax, qword ptr [a] 184 | 185 | store_result_#ID: 186 | # push result onto stack 187 | mov rax, qword ptr [a] 188 | push rax 189 | # stack size didn't change; popped one, pushed one. 190 | ` 191 | return (strings.Replace(text, "#ID", fmt.Sprintf("%d", i), -1)) 192 | } 193 | 194 | // genMinus generates assembly code to pop two values from the stack, 195 | // subtract them and store the result back on the stack. 196 | func (c *Compiler) genMinus() string { 197 | return ` 198 | # [MINUS] 199 | # ensure there are at least two arguments on the stack 200 | mov rax, qword ptr [depth] 201 | cmp rax, 2 202 | jb stack_error 203 | 204 | # pop two values 205 | pop rax 206 | mov qword ptr [a], rax 207 | pop rax 208 | mov qword ptr [b], rax 209 | 210 | # sub 211 | fld qword ptr [b] 212 | fsub qword ptr [a] 213 | fstp qword ptr [a] 214 | 215 | # push the result back onto the stack 216 | mov rax, qword ptr [a] 217 | push rax 218 | 219 | # we took two values from the stack, but added one 220 | # so the net result is the stack shrunk by one. 221 | dec qword ptr [depth] 222 | ` 223 | } 224 | 225 | // genModulus generates assembly code to pop two values from the stack, 226 | // perform a modulus-operation and store the result back on the stack. 227 | // Note we truncate things to integers in this section of the code. 228 | func (c *Compiler) genModulus() string { 229 | return ` 230 | # [MODULUS] 231 | # ensure there are at least two arguments on the stack 232 | mov rax, qword ptr [depth] 233 | cmp rax, 2 234 | jb stack_error 235 | 236 | # pop two values - rounding both to ints 237 | pop rax 238 | mov qword ptr [a], rax 239 | fld qword ptr [a] 240 | frndint 241 | fistp qword ptr [a] 242 | 243 | pop rax 244 | mov qword ptr [b], rax 245 | fld qword ptr [b] 246 | frndint 247 | fistp qword ptr [b] 248 | 249 | # now we do the modulus-magic. 250 | mov rax, qword ptr [b] 251 | mov rbx, qword ptr [a] 252 | xor rdx, rdx 253 | cqo 254 | div rbx 255 | 256 | # store the result from 'rdx'. 257 | mov qword ptr[a], rdx 258 | fild qword ptr [a] 259 | fstp qword ptr [a] 260 | mov rax, qword ptr [a] 261 | push rax 262 | 263 | # we took two values from the stack, but added one 264 | # so the net result is the stack shrunk by one. 265 | dec qword ptr [depth] 266 | ` 267 | } 268 | 269 | // genMultiply generates assembly code to pop two values from the stack, 270 | // multiply them and store the result back on the stack. 271 | func (c *Compiler) genMultiply() string { 272 | return ` 273 | # [MULTIPLY] 274 | # ensure there are at least two arguments on the stack 275 | mov rax, qword ptr [depth] 276 | cmp rax, 2 277 | jb stack_error 278 | 279 | # pop two values 280 | pop rax 281 | mov qword ptr [a], rax 282 | pop rax 283 | mov qword ptr [b], rax 284 | 285 | # multiply 286 | fld qword ptr [a] 287 | fmul qword ptr [b] 288 | fstp qword ptr [a] 289 | 290 | # push the result back onto the stack 291 | mov rax, qword ptr [a] 292 | push rax 293 | 294 | # we took two values from the stack, but added one 295 | # so the net result is the stack shrunk by one. 296 | dec qword ptr [depth] 297 | ` 298 | 299 | } 300 | 301 | // genPlus generates assembly code to pop two values from the stack, 302 | // add them and store the result back on the stack. 303 | func (c *Compiler) genPlus() string { 304 | 305 | return ` 306 | # [PLUS] 307 | # ensure there are at least two arguments on the stack 308 | mov rax, qword ptr [depth] 309 | cmp rax, 2 310 | jb stack_error 311 | 312 | # pop two values 313 | pop rax 314 | mov qword ptr [a], rax 315 | pop rax 316 | mov qword ptr [b], rax 317 | 318 | # add 319 | fld qword ptr [a] 320 | fadd qword ptr [b] 321 | fstp qword ptr [a] 322 | 323 | # push the result back onto the stack 324 | mov rax, qword ptr [a] 325 | push rax 326 | 327 | # we took two values from the stack, but added one 328 | # so the net result is the stack shrunk by one. 329 | dec qword ptr [depth] 330 | ` 331 | } 332 | 333 | // genPower generates assembly code to pop two values from the stack, 334 | // perform a power-raising and store the result back on the stack. 335 | // 336 | // Note we truncate things to integers in this section of the code. 337 | // 338 | // Note we do some comparisons here, and need to generate some (unique) labels 339 | // 340 | func (c *Compiler) genPower(i int) string { 341 | text := ` 342 | # [POWER] 343 | # ensure there are at least two arguments on the stack 344 | mov rax, qword ptr [depth] 345 | cmp rax, 2 346 | jb stack_error 347 | 348 | # pop two values - rounding both to ints 349 | pop rax 350 | mov qword ptr [a], rax 351 | fld qword ptr [a] 352 | frndint 353 | fistp qword ptr [a] 354 | 355 | pop rax 356 | mov qword ptr [b], rax 357 | fld qword ptr [b] 358 | frndint 359 | fistp qword ptr [b] 360 | 361 | # get the two values 362 | mov rax, qword ptr [b] 363 | mov rbx, qword ptr [a] 364 | 365 | # if the power is 0 we return zero 366 | cmp rbx, 0 367 | jne none_zero_#ID 368 | # store zero 369 | fldz 370 | jmp store_value_#ID 371 | 372 | none_zero_#ID: 373 | 374 | # if the power is 1 we return the original value 375 | cmp rbx, 1 376 | jne none_one_#ID 377 | mov qword ptr[a], rax 378 | fild qword ptr [a] 379 | jmp store_value_#ID 380 | 381 | none_one_#ID: 382 | # here we have rax having a value 383 | # and we have rbx having the power to raise 384 | mov rcx, rax # save the value 385 | 386 | # decrease the power by one. 387 | dec rbx 388 | again_#ID: 389 | # rax = rax * rcx (which is the original value we started with) 390 | imul rax,rcx 391 | 392 | # value too big? 393 | jo register_overflow 394 | 395 | dec rbx 396 | jnz again_#ID 397 | 398 | mov qword ptr[a], rax 399 | fild qword ptr [a] 400 | 401 | store_value_#ID: 402 | 403 | fstp qword ptr [a] 404 | 405 | # push the result back onto the stack 406 | mov rax, qword ptr [a] 407 | push rax 408 | 409 | # we took two values from the stack, but added one 410 | # so the net result is the stack shrunk by one. 411 | dec qword ptr [depth] 412 | ` 413 | 414 | return (strings.Replace(text, "#ID", fmt.Sprintf("%d", i), -1)) 415 | } 416 | 417 | // genPush generates assembly code to push a value upon the RPN stack. 418 | func (c *Compiler) genPush(value string) string { 419 | 420 | text := ` 421 | # [PUSH] 422 | # Load the value #VALUE onto the stack 423 | # Increase the value stored at [depth] to note we've a new stack-entry 424 | fld qword ptr #ESCAPED 425 | fstp qword ptr [int] 426 | mov rax, qword ptr [int] 427 | push rax 428 | inc qword ptr [depth] 429 | ` 430 | 431 | // Allow the value and the escaped value to be expanded. 432 | text = strings.Replace(text, "#VALUE", value, -1) 433 | text = strings.Replace(text, "#ESCAPED", c.escapeConstant(value), -1) 434 | 435 | return (text) 436 | } 437 | 438 | // genSin generates assembly code to pop a value from the stack, 439 | // run a sin-operation, and store the result back on the stack. 440 | func (c *Compiler) genSin() string { 441 | return ` 442 | # [SIN] 443 | # ensure there is at least one argument on the stack 444 | mov rax, qword ptr [depth] 445 | cmp rax, 1 446 | jb stack_error 447 | 448 | # pop one value 449 | pop rax 450 | mov qword ptr [a], rax 451 | 452 | # sin 453 | fld qword ptr [a] 454 | fsin 455 | fstp qword ptr [a] 456 | 457 | # push result onto stack 458 | mov rax, qword ptr [a] 459 | push rax 460 | 461 | # stack size didn't change; popped one, pushed one. 462 | ` 463 | } 464 | 465 | // genSwap generates assembly code to pop two values from the stack and 466 | // push them back, in the other order. 467 | func (c *Compiler) genSwap() string { 468 | return ` 469 | # [SWAP] 470 | # ensure there are at least two arguments on the stack 471 | mov rax, qword ptr [depth] 472 | cmp rax, 2 473 | jb stack_error 474 | 475 | pop rax 476 | pop rbx 477 | push rax 478 | push rbx 479 | # stack size didn't change; popped two, pushed two. 480 | ` 481 | } 482 | 483 | // genTan generates assembly code to pop a value from the stack, 484 | // run a tan-operation, and store the result back on the stack. 485 | func (c *Compiler) genTan() string { 486 | return ` 487 | # [TAN] 488 | # ensure there is at least one argument on the stack 489 | mov rax, qword ptr [depth] 490 | cmp rax, 1 491 | jb stack_error 492 | 493 | # pop one value 494 | pop rax 495 | mov qword ptr [a], rax 496 | 497 | # tan 498 | fld qword ptr [a] 499 | fsincos 500 | fdivr %st(0),st(1) 501 | fstp qword ptr [a] 502 | 503 | # push result onto stack 504 | mov rax, qword ptr [a] 505 | push rax 506 | ` 507 | } 508 | 509 | // genSqrt generates assembly code to pop a value from the stack, 510 | // run a square-root operation, and store the result back on the stack. 511 | func (c *Compiler) genSqrt() string { 512 | return ` 513 | # [SQRT] 514 | # ensure there is at least one argument on the stack 515 | mov rax, qword ptr [depth] 516 | cmp rax, 1 517 | jb stack_error 518 | 519 | # pop one value 520 | pop rax 521 | mov qword ptr [a], rax 522 | 523 | # sqrt 524 | fld qword ptr [a] 525 | fsqrt 526 | fstp qword ptr [a] 527 | 528 | # push result onto stack 529 | mov rax, qword ptr [a] 530 | push rax 531 | 532 | # stack size didn't change; popped one, pushed one. 533 | ` 534 | } 535 | -------------------------------------------------------------------------------- /compiler/generator_test.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import "testing" 4 | 5 | // TestEscape tests excaping numbers to constants 6 | func TestEscape(t *testing.T) { 7 | 8 | tests := []struct { 9 | input string 10 | expected string 11 | }{ 12 | {"3", "const_3"}, 13 | {"0.03", "const_0_03"}, 14 | {"-3", "const_neg_3"}, 15 | {"-3.3", "const_neg_3_3"}, 16 | } 17 | 18 | for _, text := range tests { 19 | 20 | c := New("") 21 | 22 | got := c.escapeConstant(text.input) 23 | 24 | if got != text.expected { 25 | t.Errorf("Expected '%s' to become '%s', got '%s'", 26 | text.input, text.expected, got) 27 | } 28 | } 29 | } 30 | 31 | // TestGenerators just calls the various generating methods, to ensure 32 | // they're covered. 33 | // Since there is no logic in them testing them is pretty pointless. 34 | func TestGenerators(t *testing.T) { 35 | 36 | // create 37 | c := New("2 3+") 38 | 39 | // misc 40 | c.genPush("3.4") 41 | 42 | // simple 43 | c.genPlus() 44 | c.genMinus() 45 | c.genMultiply() 46 | c.genDivide() 47 | 48 | // misc 49 | c.genModulus() 50 | c.genPower(1) 51 | 52 | // complex 53 | c.genAbs() 54 | c.genCos() 55 | c.genSin() 56 | c.genSqrt() 57 | c.genTan() 58 | 59 | // stack 60 | c.genDup() 61 | c.genSwap() 62 | } 63 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skx/math-compiler 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /instructions/instructions.go: -------------------------------------------------------------------------------- 1 | // Package instructions contains a series of types. 2 | // 3 | // We parse the program we're given into a series of tokens, then 4 | // later convert those tokens into an internal-form, using these 5 | // instructions. 6 | // 7 | // At generation time we emit a header, a footer, and a series of 8 | // snippets - one for each logical instruction. 9 | package instructions 10 | 11 | // InstructionType holds the type of the instruction. 12 | type InstructionType byte 13 | 14 | const ( 15 | // Push is used to generate code to push a number onto the stack. 16 | Push InstructionType = 'p' 17 | 18 | // Plus means to pop two items from the stack and push the result 19 | // of adding them. 20 | Plus InstructionType = '+' 21 | 22 | // Minus means to pop two items from the stack and push the result 23 | // of subtracting them. 24 | Minus InstructionType = '-' 25 | 26 | // Multiply means to pop two items from the stack and push the result 27 | // of multiplying them. 28 | Multiply InstructionType = '*' 29 | 30 | // Divide means to pop two items from the stack and push the result 31 | // of dividing them. 32 | Divide InstructionType = '/' 33 | 34 | // Power means to pop two items from the stack and push the result 35 | // of raising one to the power of the other. 36 | Power InstructionType = '^' 37 | 38 | // Modulus means to pop two items from the stack and push the result 39 | // of running a modulus operation. 40 | Modulus InstructionType = '%' 41 | 42 | // Factorial allows calculating the factorials. 43 | Factorial InstructionType = '!' 44 | 45 | // Abs is used to pop a value from the stack and push the absolute 46 | // value back. 47 | Abs InstructionType = 'a' 48 | 49 | // Sin is used to pop a value from the stack and push the result 50 | // of sin() back. 51 | Sin InstructionType = 's' 52 | 53 | // Cos is used to pop a value from the stack and push the result 54 | // of cos() back. 55 | Cos InstructionType = 'c' 56 | 57 | // Tan is used to pop a value from the stack and push the result 58 | // of tan() back. 59 | Tan InstructionType = 't' 60 | 61 | // Sqrt is used to pop a value from the stack and push the result 62 | // of calculating its square-root back. 63 | Sqrt InstructionType = 'q' 64 | 65 | // Swap swaps the position of the top two stack-items. 66 | Swap InstructionType = 'S' 67 | 68 | // Dup duplicates the stacks topmost value. 69 | Dup InstructionType = 'D' 70 | ) 71 | 72 | // Instruction holds a single thing that the compiler must generate code for. 73 | // (The value is only used when a float is to be pushed upon the stack.) 74 | type Instruction struct { 75 | 76 | // Type holds the type of instruction this object represents 77 | Type InstructionType 78 | 79 | // Value holds the value of a number to be pushed upon the RPN stack. 80 | Value string 81 | } 82 | -------------------------------------------------------------------------------- /lexer/lexer.go: -------------------------------------------------------------------------------- 1 | // Package lexer converts our input-program into a series of tokens, 2 | // which we can later parse and compile. 3 | package lexer 4 | 5 | import ( 6 | "strings" 7 | 8 | "github.com/skx/math-compiler/token" 9 | ) 10 | 11 | // Lexer holds our object-state. 12 | type Lexer struct { 13 | position int //current character position 14 | readPosition int //next character position 15 | ch rune //current character 16 | characters []rune //rune slice of input string 17 | } 18 | 19 | // New a Lexer instance from string input. 20 | func New(input string) *Lexer { 21 | l := &Lexer{characters: []rune(input)} 22 | l.readChar() 23 | return l 24 | } 25 | 26 | // read one forward character 27 | func (l *Lexer) readChar() { 28 | if l.readPosition >= len(l.characters) { 29 | l.ch = rune(0) 30 | } else { 31 | l.ch = l.characters[l.readPosition] 32 | } 33 | l.position = l.readPosition 34 | l.readPosition++ 35 | } 36 | 37 | // NextToken to read next token, skipping the white space. 38 | func (l *Lexer) NextToken() token.Token { 39 | var tok token.Token 40 | l.skipWhitespace() 41 | 42 | switch l.ch { 43 | case rune('+'): 44 | tok = newToken(token.PLUS, l.ch) 45 | case rune('%'): 46 | tok = newToken(token.MOD, l.ch) 47 | case rune('!'): 48 | tok = newToken(token.FACTORIAL, l.ch) 49 | case rune('^'): 50 | tok = newToken(token.POWER, l.ch) 51 | case rune('-'): 52 | // "-3" is "-3", "-3.4" is "-3.4", but "3 - 4" is -1 (via the distinct tokens "3", "-", "4".) 53 | if isDigit(l.peekChar()) { 54 | 55 | // swallow the - 56 | l.readChar() 57 | 58 | // read an int/float 59 | tok = l.readDecimal() 60 | 61 | // ensure the sign is not lost. 62 | tok.Literal = "-" + tok.Literal 63 | 64 | } else { 65 | tok = newToken(token.MINUS, l.ch) 66 | } 67 | case rune('/'): 68 | tok = newToken(token.SLASH, l.ch) 69 | case rune('*'): 70 | tok = newToken(token.ASTERISK, l.ch) 71 | case rune(0): 72 | tok.Literal = "" 73 | tok.Type = token.EOF 74 | default: 75 | if isDigit(l.ch) { 76 | return l.readDecimal() 77 | } 78 | 79 | lit := l.readIdentifier() 80 | tok.Type = token.LookupIdentifier(lit) 81 | if tok.Type == token.ERROR { 82 | tok.Literal = "Unknown token " + lit 83 | } else { 84 | tok.Literal = lit 85 | } 86 | } 87 | l.readChar() 88 | return tok 89 | } 90 | 91 | // return new token 92 | func newToken(tokenType token.Type, ch rune) token.Token { 93 | return token.Token{Type: tokenType, Literal: string(ch)} 94 | } 95 | 96 | // skip white space 97 | func (l *Lexer) skipWhitespace() { 98 | for isWhitespace(l.ch) { 99 | l.readChar() 100 | } 101 | } 102 | 103 | // readNumber handles reading a number, comprising of digits 0-9. 104 | func (l *Lexer) readNumber() string { 105 | str := "" 106 | 107 | // We only accept digits. 108 | accept := "0123456789" 109 | 110 | for strings.Contains(accept, string(l.ch)) { 111 | str += string(l.ch) 112 | l.readChar() 113 | } 114 | return str 115 | } 116 | 117 | // read a decimal / floating point number. 118 | func (l *Lexer) readDecimal() token.Token { 119 | 120 | // 121 | // Read an integer-number. 122 | // 123 | integer := l.readNumber() 124 | 125 | // 126 | // We might have more content: 127 | // 128 | // .[digits] -> Which converts us from an int to a float. 129 | // 130 | if l.ch == rune('.') && isDigit(l.peekChar()) { 131 | 132 | // 133 | // OK here we think we've got a float. 134 | // 135 | // Skip the period. 136 | // 137 | l.readChar() 138 | 139 | // 140 | // Read the fractional part. 141 | // 142 | fraction := l.readNumber() 143 | return token.Token{Type: token.NUMBER, Literal: integer + "." + fraction} 144 | } 145 | return token.Token{Type: token.NUMBER, Literal: integer} 146 | 147 | } 148 | 149 | // peek character 150 | func (l *Lexer) peekChar() rune { 151 | if l.readPosition >= len(l.characters) { 152 | return rune(0) 153 | } 154 | return l.characters[l.readPosition] 155 | } 156 | 157 | // is white space 158 | func isWhitespace(ch rune) bool { 159 | return ch == rune(' ') || ch == rune('\t') || ch == rune('\n') || ch == rune('\r') 160 | } 161 | 162 | // is Digit 163 | func isDigit(ch rune) bool { 164 | return rune('0') <= ch && ch <= rune('9') 165 | } 166 | 167 | // readIdentifier is designed to read an identifier which means a string 168 | // such as `sin`, `cos`, `tan`. 169 | func (l *Lexer) readIdentifier() string { 170 | 171 | id := "" 172 | 173 | // 174 | // Build up our identifier, handling only valid characters. 175 | // 176 | for isIdentifier(l.ch) { 177 | id += string(l.ch) 178 | l.readChar() 179 | } 180 | 181 | return id 182 | } 183 | 184 | // determinate ch is identifier or not 185 | func isIdentifier(ch rune) bool { 186 | return !isDigit(ch) && !isWhitespace(ch) && ch != rune(0) 187 | } 188 | -------------------------------------------------------------------------------- /lexer/lexer_test.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/skx/math-compiler/token" 7 | ) 8 | 9 | // Trivial test of the parsing of numbers. 10 | func TestParseNumbers(t *testing.T) { 11 | input := `3 43 -17 -3` 12 | 13 | tests := []struct { 14 | expectedType token.Type 15 | expectedLiteral string 16 | }{ 17 | {token.NUMBER, "3"}, 18 | {token.NUMBER, "43"}, 19 | {token.NUMBER, "-17"}, 20 | {token.NUMBER, "-3"}, 21 | {token.EOF, ""}, 22 | } 23 | l := New(input) 24 | for i, tt := range tests { 25 | tok := l.NextToken() 26 | if tok.Type != tt.expectedType { 27 | t.Fatalf("tests[%d] - tokentype wrong, expected=%q, got=%q", i, tt.expectedType, tok.Type) 28 | } 29 | if tok.Literal != tt.expectedLiteral { 30 | t.Fatalf("tests[%d] - Literal wrong, expected=%q, got=%q", i, tt.expectedLiteral, tok.Literal) 31 | } 32 | } 33 | } 34 | 35 | // Trivial test of parsing floats. 36 | func TestParseFloats(t *testing.T) { 37 | input := `3.14 4.3 -1.7 -2.13 sin ` 38 | 39 | tests := []struct { 40 | expectedType token.Type 41 | expectedLiteral string 42 | }{ 43 | {token.NUMBER, "3.14"}, 44 | {token.NUMBER, "4.3"}, 45 | {token.NUMBER, "-1.7"}, 46 | {token.NUMBER, "-2.13"}, 47 | {token.SIN, "sin"}, 48 | {token.EOF, ""}, 49 | } 50 | l := New(input) 51 | for i, tt := range tests { 52 | tok := l.NextToken() 53 | if tok.Type != tt.expectedType { 54 | t.Fatalf("tests[%d] - tokentype wrong, expected=%q, got=%q", i, tt.expectedType, tok) 55 | } 56 | if tok.Literal != tt.expectedLiteral { 57 | t.Fatalf("tests[%d] - Literal wrong, expected=%q, got=%q", i, tt.expectedLiteral, tok.Literal) 58 | } 59 | } 60 | } 61 | 62 | // Trivial test of the parsing of operators. 63 | func TestParseOperators(t *testing.T) { 64 | input := `+ - * / % ^ - !` 65 | 66 | tests := []struct { 67 | expectedType token.Type 68 | expectedLiteral string 69 | }{ 70 | {token.PLUS, "+"}, 71 | {token.MINUS, "-"}, 72 | {token.ASTERISK, "*"}, 73 | {token.SLASH, "/"}, 74 | {token.MOD, "%"}, 75 | {token.POWER, "^"}, 76 | {token.MINUS, "-"}, 77 | {token.FACTORIAL, "!"}, 78 | {token.EOF, ""}, 79 | } 80 | l := New(input) 81 | for i, tt := range tests { 82 | tok := l.NextToken() 83 | if tok.Type != tt.expectedType { 84 | t.Fatalf("tests[%d] - tokentype wrong, expected=%q, got=%q", i, tt.expectedType, tok.Type) 85 | } 86 | if tok.Literal != tt.expectedLiteral { 87 | t.Fatalf("tests[%d] - Literal wrong, expected=%q, got=%q", i, tt.expectedLiteral, tok.Literal) 88 | } 89 | } 90 | } 91 | 92 | // Trivial test of the parsing invalid input 93 | func TestParseBogus(t *testing.T) { 94 | input := `steve 3` 95 | 96 | tests := []struct { 97 | expectedType token.Type 98 | expectedLiteral string 99 | }{ 100 | {token.ERROR, "Unknown token steve"}, 101 | {token.NUMBER, "3"}, 102 | {token.EOF, ""}, 103 | } 104 | l := New(input) 105 | for i, tt := range tests { 106 | tok := l.NextToken() 107 | if tok.Type != tt.expectedType { 108 | t.Fatalf("tests[%d] - tokentype wrong, expected=%q, got=%q", i, tt.expectedType, tok.Type) 109 | } 110 | if tok.Literal != tt.expectedLiteral { 111 | t.Fatalf("tests[%d] - Literal wrong, expected=%q, got=%q", i, tt.expectedLiteral, tok.Literal) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // This is the main-driver for our compiler. 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | 12 | "github.com/skx/math-compiler/compiler" 13 | ) 14 | 15 | func main() { 16 | 17 | // 18 | // Look for flags. 19 | // 20 | debug := flag.Bool("debug", false, "Insert debug \"stuff\" in our generated output.") 21 | compile := flag.Bool("compile", false, "Compile the program, via invoking gcc.") 22 | program := flag.String("filename", "a.out", "The program to write to.") 23 | run := flag.Bool("run", false, "Run the binary, post-compile.") 24 | flag.Parse() 25 | 26 | // 27 | // If we're running we're also compiling 28 | // 29 | if *run { 30 | *compile = true 31 | } 32 | 33 | // 34 | // Ensure we have an expression as our single argument. 35 | // 36 | if len(flag.Args()) != 1 { 37 | fmt.Printf("Usage: math-compiler 'expression'\n") 38 | os.Exit(1) 39 | } 40 | 41 | // 42 | // Create a compiler-object, with the program as input. 43 | // 44 | comp := compiler.New(flag.Args()[0]) 45 | 46 | // 47 | // Are we inserting debugging "stuff" ? 48 | // 49 | if *debug { 50 | comp.SetDebug(true) 51 | } 52 | 53 | // 54 | // Compile 55 | // 56 | out, err := comp.Compile() 57 | if err != nil { 58 | fmt.Printf("Error compiling: %s\n", err.Error()) 59 | os.Exit(1) 60 | } 61 | 62 | // 63 | // If we're not compiling the assembly language text which was 64 | // produced then we just write the program to STDOUT, and terminate. 65 | // 66 | if !*compile { 67 | fmt.Printf("%s", out) 68 | return 69 | } 70 | 71 | // 72 | // OK we're compiling the program, via gcc. 73 | // 74 | gcc := exec.Command("gcc", "-static", "-o", *program, "-x", "assembler", "-") 75 | gcc.Stdout = os.Stdout 76 | gcc.Stderr = os.Stderr 77 | 78 | // 79 | // We'll pipe our generated-program to STDIN of gcc, via a 80 | // temporary buffer-object. 81 | // 82 | var b bytes.Buffer 83 | b.Write([]byte(out)) 84 | gcc.Stdin = &b 85 | 86 | // 87 | // Run gcc. 88 | // 89 | err = gcc.Run() 90 | if err != nil { 91 | fmt.Printf("Error launching gcc: %s\n", err) 92 | os.Exit(1) 93 | } 94 | 95 | // 96 | // Running the binary too? 97 | // 98 | if *run { 99 | exe := exec.Command(*program) 100 | exe.Stdout = os.Stdout 101 | exe.Stderr = os.Stderr 102 | err = exe.Run() 103 | if err != nil { 104 | fmt.Printf("Error launching %s: %s\n", *program, err) 105 | os.Exit(1) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Simple test-driver to exercise our compiler. 4 | # 5 | # We compile some simple test-cases and test that the results match what 6 | # we expect. 7 | # 8 | 9 | 10 | # Compile an expression and compare the result with a fixed value 11 | # 12 | # If the optional third argument is present, and non-empty, then we 13 | # return the full output from the execution. Otherwise just the last 14 | # token. 15 | test_compile() { 16 | input="$1" 17 | result="$2" 18 | full="$3" 19 | 20 | # 21 | # Do this the long way round so we have assembly file for 22 | # inspection if/when a test fails. 23 | # 24 | rm -f test.s test || true 25 | go run main.go -- "${input}" > test.s 26 | gcc -static -o ./test test.s 27 | 28 | # 29 | # Run the test. 30 | # 31 | out=`./test` 32 | 33 | # 34 | # If we're not doing a "full" match we only take the last token 35 | # 36 | if [ "${full}" = "" ]; then 37 | out=$(echo "$out" | awk '{print $NF}') 38 | fi 39 | 40 | if [ "${result}" = "${out}" ]; then 41 | echo "Expected output found for '$input' [$result] " 42 | rm test test.s 43 | else 44 | echo "Expected output of '$input' is '$result' - got '${out}' instead" 45 | exit 1 46 | fi 47 | 48 | } 49 | 50 | 51 | # Simple operations 52 | test_compile '1 2 3 4 + + +' 10 53 | test_compile '3 4 5 * *' 60 54 | test_compile '20 10 2 - -' 12 55 | test_compile '20 4 2 / / ' 10 56 | 57 | # Division by zero 58 | test_compile '3 4 /' '0.75' 59 | test_compile '3 0 /' 'Attempted division by zero. Aborting' 'full' 60 | test_compile '3 5 5 - /' 'Attempted division by zero. Aborting' 'full' 61 | 62 | # Missing arguments upon the stack 63 | test_compile '4 +' 'Insufficient entries on the stack. Aborting' 'full' 64 | test_compile '3 sin -' 'Insufficient entries on the stack. Aborting' 'full' 65 | 66 | # Too many arguments on the stack 67 | test_compile '3 3 3 +' 'Too many entries remaining on the stack. Aborting' 'full' 68 | 69 | 70 | # modulus 71 | test_compile '1 4 %' 1 72 | test_compile '2 4 %' 2 73 | test_compile '3 4 %' 3 74 | test_compile '4 4 %' 0 75 | test_compile '5 4 %' 1 76 | test_compile '6 4 %' 2 77 | test_compile '7 4 %' 3 78 | test_compile '8 4 %' 0 79 | test_compile '9 4 %' 1 80 | test_compile '10 4 %' 2 81 | test_compile '11 4 %' 3 82 | test_compile '12 4 %' 0 83 | 84 | # powers of two - the manual-way 85 | test_compile '2 2 *' 4 86 | test_compile '2 2 2 * *' 8 87 | test_compile '2 2 2 2 * * *' 16 88 | test_compile '2 2 2 2 2 * * * *' 32 89 | test_compile '2 2 2 2 2 2 * * * * *' 64 90 | test_compile '2 2 2 2 2 2 2 * * * * * *' 128 91 | test_compile '2 2 2 2 2 2 2 2 * * * * * * *' 256 92 | test_compile '2 2 2 2 2 2 2 2 2 * * * * * * * *' 512 93 | test_compile '2 2 2 2 2 2 2 2 2 2 * * * * * * * * *' 1024 94 | 95 | 96 | # Add an extreme example of calculating 2^24: 97 | inp="2 2 *" 98 | for i in $(seq 1 22 ) ; do 99 | inp="${inp} 2 *" 100 | done 101 | test_compile "$inp" 1.67772e+07 102 | 103 | 104 | # powers of two - the simple way 105 | test_compile '2 0 ^' 0 106 | test_compile '2 1 ^' 2 107 | test_compile '2 2 ^' 4 108 | test_compile '2 3 ^' 8 109 | test_compile '2 4 ^' 16 110 | test_compile '2 5 ^' 32 111 | test_compile '2 6 ^' 64 112 | test_compile '2 7 ^' 128 113 | test_compile '2 8 ^' 256 114 | test_compile '2 16 ^' 65536 115 | test_compile '2 30 ^' 1.07374e+09 116 | test_compile '2 300 ^' 'Overflow - value out of range. Aborting' 'full' 117 | 118 | # factorials 119 | test_compile '-3 !' 0 120 | test_compile '0 !' 0 121 | test_compile '1 !' 1 122 | test_compile '2 !' 2 123 | test_compile '3 !' 6 124 | test_compile '4 !' 24 125 | test_compile '5 !' 120 126 | test_compile '6 !' 720 127 | test_compile '5 5 + !' 3.6288e+06 # 3628800 128 | test_compile '3 300 !' 'Overflow - value out of range. Aborting' 'full' 129 | 130 | # division 131 | test_compile '3 2 /' 1.5 132 | test_compile '5 2 /' 2.5 133 | 134 | # abs 135 | test_compile '3 abs' 3 136 | test_compile '3 9 - abs' 6 137 | test_compile '-3 abs' 3 138 | 139 | # sqrt 140 | test_compile '9 sqrt' 3 141 | test_compile '81 sqrt sqrt' 3 142 | test_compile '81 sqrt sqrt sqrt' 1.73205 143 | 144 | # circles 145 | test_compile '1 sin' 0.841471 146 | test_compile '1 cos' 0.540302 147 | test_compile '1 tan' 1.55741 148 | 149 | # swap 150 | test_compile '3 5 -' -2 151 | test_compile '3 5 swap -' 2 152 | 153 | # dup 154 | test_compile '3 sqrt dup *' 3 155 | test_compile '3 dup ^' 27 156 | 157 | exit 0 158 | -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | // Package token contains the tokens that the lexer will produce when 2 | // parsing an input-expression. 3 | package token 4 | 5 | // Type is a string 6 | type Type string 7 | 8 | // Token struct represent the lexer token 9 | type Token struct { 10 | Type Type 11 | Literal string 12 | } 13 | 14 | // pre-defined Type 15 | const ( 16 | EOF = "EOF" 17 | ERROR = "ERROR" 18 | NUMBER = "NUMBER" 19 | IDENT = "IDENT" 20 | 21 | // simple operations 22 | PLUS = "+" 23 | MINUS = "-" 24 | ASTERISK = "*" 25 | SLASH = "/" 26 | 27 | // advanced operations 28 | MOD = "%" 29 | POWER = "^" 30 | FACTORIAL = "!" 31 | 32 | // misc 33 | E = "e" 34 | PI = "pi" 35 | 36 | // complex operations 37 | ABS = "abs" 38 | COS = "cos" 39 | SIN = "sin" 40 | SQRT = "sqrt" 41 | TAN = "tan" 42 | 43 | // stack operations 44 | DUP = "dup" 45 | SWAP = "swap" 46 | ) 47 | 48 | // reversed keywords 49 | var keywords = map[string]Type{ 50 | "abs": ABS, 51 | "cos": COS, 52 | "dup": DUP, 53 | "e": E, 54 | "pi": PI, 55 | "sin": SIN, 56 | "sqrt": SQRT, 57 | "swap": SWAP, 58 | "tan": TAN, 59 | } 60 | 61 | // LookupIdentifier used to determinate whether identifier is keyword nor not 62 | func LookupIdentifier(identifier string) Type { 63 | if tok, ok := keywords[identifier]; ok { 64 | return tok 65 | } 66 | return ERROR 67 | } 68 | -------------------------------------------------------------------------------- /token/token_test.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Test looking up values succeeds. 8 | func TestLookup(t *testing.T) { 9 | 10 | for key, val := range keywords { 11 | 12 | // Obviously this will pass. 13 | if LookupIdentifier(string(key)) != val { 14 | t.Errorf("Lookup of %s failed", key) 15 | } 16 | 17 | } 18 | } 19 | 20 | // Test looking up unknown-values. 21 | func TestLookupFailures(t *testing.T) { 22 | 23 | keywords := []string{"foo", "bar", "baz"} 24 | 25 | for _, val := range keywords { 26 | 27 | // Obviously this will pass. 28 | if LookupIdentifier(string(val)) != ERROR { 29 | t.Errorf("Lookup of %s was expected to fail, but didn't", val) 30 | } 31 | 32 | } 33 | } 34 | --------------------------------------------------------------------------------