├── .github └── logo.svg ├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── changelog.md ├── docs ├── cmd.md ├── grammar.md ├── lang.md └── libraries.md ├── env ├── create.go ├── env.go ├── native.go └── types.go ├── examples ├── linked_list.fizz ├── number_guesser.fizz └── user_database │ ├── entries.fizz │ ├── user.fizz │ └── users.txt ├── expr ├── evaluate.go ├── expr.go └── parse.go ├── go.mod ├── go.sum ├── interp └── interp.go ├── lexer ├── lexer.go └── token_types.go ├── lib ├── autodocs.py ├── build.py ├── io │ ├── io.go │ └── io_docs.md ├── lib.go ├── math │ ├── math.go │ └── math_docs.md ├── print.go └── str │ ├── str.go │ └── str_docs.md ├── main.go ├── run.go ├── stmt ├── execute.go ├── parse.go └── stmt.go ├── term ├── cmd.go ├── handler.go ├── help.txt └── parser.go ├── test ├── cases │ ├── invalid_expr.fizz │ ├── invalid_stmt.fizz │ ├── valid_expr.fizz │ └── valid_stmt.fizz └── fizz_test.go └── util └── util.go /.github/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Buildable binaries 2 | bin 3 | export 4 | 5 | # Temp files for development 6 | dev_files 7 | main.fizz 8 | 9 | # vscode 10 | .vscode 11 | 12 | # Buildable 13 | lib/include.go 14 | lib/_libdump 15 | lib/*/export.go -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 jesperkha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | Logo 4 | 5 |

Fizz, the programming language

6 | 7 |

8 | Interpreted dynamic programming language built with Go. 9 |
10 | Documentation » 11 |
12 |
13 | Examples 14 | · 15 | Report Bug 16 | · 17 | Download 18 |

19 |
20 | 21 |
22 | 23 |
24 | Table of Contents 25 | 32 |
33 | 34 |
35 | 36 | ## About 37 | 38 | Fizz is a dynamic and interpreted programming language built with Go. It is strongly typed and comes with very readable and accurate error messages. Fizz has most of the standard functionality that you would expect from modern programming languages. The library system also allows the user to implement their own features as Go functions and port them directly into Fizz. If you like this project, consider giving it a star 😉 39 | 40 | ### Features 41 | 42 | - Variables, conditionals, and loops 43 | - Functions, arrays, and objects 44 | - File imports and libraries 45 | - Clean syntax and simple grammar 46 | 47 |
48 | 49 | ## Installation 50 | 51 | Prebuilt binary of the [latest release (v1.1.0)](https://github.com/jesperkha/Fizz/releases/tag/v1.1.0). 52 | 53 | You can also build from source. However, building from source from a non-release branch does not guarantee that everything works as expected as some things may be undergoing changes. 54 | 55 | 1. Clone repo 56 | 2. Run the `build.sh` file 57 | 58 | ### Syntax highlighting 59 | 60 | Finally, there is also optional, but recommended, [syntax highlighting](https://github.com/jesperkha/fizz-extensions) extensions for both Visual Studio Code and micro. 61 | 62 |
63 | 64 | ## Documentation 65 | 66 | You can read the [full language documentation](./docs/lang.md) to learn about all of Fizz's syntax. It is also recommended to quickly skim over [the language grammar](./docs/grammar.md) to make sure you undestand the basics of how Fizz is structured (don't worry, it's _very_ similar to most modern programming languages). 67 | 68 | Make sure to check out [the command-line basics](./docs/cmd.md) too so you know how to run your code and also which configurations you can apply. 69 | 70 |
71 | 72 | ## Running a program 73 | 74 | [Full documentation on command-line basics](./docs/cmd.md) 75 | 76 | ### Terminal mode 77 | 78 | Running the interpreter without giving a filename will run the terminal mode where you can run any valid Fizz code live. Errors are printed but the program is not terminated. Newlines are supported for blocks and the code will not be executed until the block is closed. 79 | 80 | ### Run file 81 | 82 | Running the interpreter and giving a filename simply runs the code in the file and halts if an error occurs. Fizz files must end with the `.fizz` suffix. Both of the following are valid: 83 | 84 | ```console 85 | $ ./fizz myFile.fizz 86 | $ ./fizz myFile 87 | ``` 88 | 89 |
90 | 91 | ## Code examples 92 | 93 | Some simple code examples written in Fizz. There are more and bigger examples in the `examples` directory. All of the features used here and many more are thoroughly documented in the [documentation page](./docs/lang.md). 94 | 95 |
96 | 97 | Write to a file: 98 | 99 | ```go 100 | include "io"; 101 | include "str"; 102 | 103 | define Person { 104 | name, age 105 | } 106 | 107 | func writePerson(person) { 108 | if person == nil : person.name == "" { 109 | error "Please enter a valid person"; 110 | } 111 | 112 | io.appendFile("names.txt", str.format(person)); 113 | } 114 | 115 | john := Person("John", 59); 116 | writePerson(john); 117 | ``` 118 | 119 |
120 | 121 | Find max and min numbers in array: 122 | 123 | ```go 124 | include "str"; 125 | 126 | arr := [5, 3, 7.5, 8, 2]; 127 | max := 0; 128 | min := 999; 129 | 130 | range n in arr { 131 | if n > max { 132 | max = n; 133 | } 134 | if n < min { 135 | min = n; 136 | } 137 | } 138 | 139 | print "Max: " + str.toString(max); 140 | print "Min: " + str.toString(min); 141 | ``` 142 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | echo "[INFO] Adding libraries" 4 | python lib/build.py 5 | 6 | echo "[INFO] Generating library docs" 7 | python lib/autodocs.py 8 | 9 | echo "[INFO] Building binary" 10 | [ ! -d "./bin" ] && mkdir bin && echo "[INFO] Created /bin directory" 11 | 12 | CMD="go build -o bin/fizz.exe ." 13 | echo "[CMD] $CMD" 14 | $CMD 15 | 16 | echo -n "[INFO] Finished build of " && ./bin/fizz --version -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Fizz update changelog 2 | 3 | ## Version 1.2.0 4 | 5 | **Whats new:** 6 | 7 | - Better library assembler 8 | 9 | **Bug fixes and more:** 10 | 11 | - Fixed bug where unary expressions would be parsed incorrectly 12 | - Validator for program flags 13 | - Added better parsing for parenthesis coupling 14 | - Fixed incorrect parsing for binary operations 15 | 16 |
17 | 18 | ## Version 1.1.0 19 | 20 | **Whats new:** 21 | 22 | - New `docs` subcommand to view functions in a given library 23 | - Newline and tab characters for strings 24 | 25 | **Bug fixes and more:** 26 | 27 | - New terminal argument parser 28 | 29 |
30 | 31 | ## Version 1.0.0 32 | 33 | **Whats new:** 34 | 35 | - Enums 36 | - New repeat statement 37 | - Range statement 38 | 39 | **Bug fixes and more:** 40 | 41 | - Changed env to allow acces to definitions / reassignments _after_ a closure was formed 42 | - Added new recursive equality check for objects and arrays 43 | - Implemented callstack and -f flag to show it 44 | - Error for exceeding recursion limit 45 | - Fixed bug where environments would be referenced and not copied 46 | 47 |
48 | 49 | ## Version 0.6.0 50 | 51 | **Whats new:** 52 | 53 | - Libraries 54 | - Error statement 55 | 56 | **Bug fixes and more:** 57 | 58 | - Automatic documentation for libraries 59 | 60 |
61 | 62 | ## Version 0.5.0 63 | 64 | **Whats new:** 65 | 66 | - Arrays 67 | - New `:=` operator for variable declaration, removed `var` statement (temporary) 68 | 69 | **Bug fixes and more:** 70 | 71 | - Prettier print for values 72 | - Fixed the semicolon error to now actually show when there is a semicolon missing, instead of just giving an expression error 73 | - Fixed error that would occur when calling group expressions 74 | 75 |
76 | 77 | ## Version 0.4.0 78 | 79 | **Whats new:** 80 | 81 | - Closures 82 | 83 | **Bug fixes and more:** 84 | 85 | - Errors for circular imports 86 | - Patched bug where env was cleared in terminal mode 87 | 88 |
89 | 90 | ## Version 0.3.0 91 | 92 | **Whats new:** 93 | 94 | - Added file imports 95 | 96 | **Bug fixes and more:** 97 | 98 | - Fixed error messages for break, skip, and return errors 99 | - Function error traceback to origin file 100 | - New env structure 101 | - Added changelog 102 | -------------------------------------------------------------------------------- /docs/cmd.md: -------------------------------------------------------------------------------- 1 | # Fizz command line use 2 | 3 | You run a Fizz file by running the `fizz` program followed by a path to a file or just the filename if it is in the current directory. Note that you don't need to include the `.fizz` suffix, it's optional. 4 | 5 | ```console 6 | $ fizz myFile 7 | ``` 8 | 9 | As also mentioned in the readme, running `fizz` with no arguments runs the terminal mode. You can then write any valid Fizz code and run it live. Errors do not terminate the session. 10 | 11 | ```console 12 | $ fizz 13 | type 'exit' to terminate session 14 | 1 : print "hello"; 15 | hello 16 | ``` 17 | 18 | Creating a new line after an opening brace `{` will auto indent for you and not execute the code until you close it again with a closing brace `}`: 19 | 20 | ```console 21 | $ fizz 22 | type 'exit' to terminate session 23 | 1 : func main() { 24 | 2 : print "hello"; 25 | 3 : } 26 | 4 : 27 | 5 : main(); 28 | hello 29 | ``` 30 | 31 | You can at any point type `exit` followed by enter to close the program. Using `ctrl-C` is also possible, but not recommended. 32 | 33 |
34 | 35 | ## Flags 36 | 37 | There are multiple flags you can use, however, some will only take effect when running a file. 38 | 39 |
40 | 41 | **Info flags** 42 | 43 | - `--help` print information on how to use the program and also all available flags 44 | - `--version` print the version of the program 45 | 46 |
47 | 48 | **Config flags** 49 | 50 | - `-f` print function callstack upon error 51 | - `-e` print the global environment after program finish 52 | 53 |
54 | 55 | ## Subcommands 56 | 57 | - **`help`** 58 | 59 | ```console 60 | $ fizz help [command] 61 | ``` 62 | 63 | Prints out a help message for the given command. 64 | 65 | - **`docs`** 66 | 67 | ```console 68 | $ fizz docs [lib name] 69 | ``` 70 | 71 | When given a name of a valid library (`str`, `io` etc), it will print out all of the functions defined in that library. If you have made your own library and some functions are not showing up make sure to re-run `autodocs.py`. 72 | -------------------------------------------------------------------------------- /docs/grammar.md: -------------------------------------------------------------------------------- 1 | # Fizz grammar 2 | 3 | ```py 4 | # Fizz program 5 | fizzProgram -> declaration* 6 | 7 | # Declarations 8 | declaration -> varDec | funcDec | objDec | statement 9 | varDec -> identifier ":=" expression ";" 10 | funcDec -> "func" "(" identifier? ("," identifier)* ")" block 11 | objDec -> "define" identifier "{" identifier* "}" 12 | 13 | # Statements 14 | statement -> exprStmt | printStmt | exitStmt | errorStmt | ifStmt | whileStmt | 15 | returnStmt | importStmt | includeStmt | assignStmt | enumStmt | 16 | repeatStmt | rangeStmt | block 17 | exprStmt -> expression ";" 18 | printStmt -> "print" expression ";" 19 | exitStmt -> "exit" expression? ";" 20 | errorStmt -> "error" expression ";" 21 | ifStmt -> "if" expression block ("else" block)? 22 | whileStmt -> "while" expression block 23 | returnStmt -> "return" expression? ";" 24 | importStmt -> "import" string ";" 25 | includeStmt -> "include" string ";" 26 | assignStmt -> (getter | identifier) "=" expression ";" 27 | enumStmt -> "enum" "{" identifier* "}" 28 | repeatStmt -> "repeat" expression block 29 | rangeStmt -> "range" identifier "in" rangeable block 30 | block -> "{" declaration* "}" 31 | 32 | # Expressions 33 | expression -> literal | unary | binary | group | call | array | getter | index | rangeable 34 | literal -> "true" | "false" | "nil" | identifier | number | string 35 | unary -> ("-", "!", "type") expression 36 | binary -> expression operator expression 37 | group -> "(" expression ")" 38 | call -> expression "(" expression? ("," expression)* ")"* 39 | array -> "[" expression? ("," expression)* "]" 40 | getter -> expression "." identfier 41 | index -> array "[" expression "]" 42 | rangeable -> array | expression ("," expression)* 43 | 44 | # Operators 45 | operator -> "+" | "-" | "*" | "/" | "^" | "%" | "&" | 46 | ":" | "==" | "!=" | ">=" | "<=" | "<" | 47 | ">" | "in" 48 | assignOp -> "=" | ":=" 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/lang.md: -------------------------------------------------------------------------------- 1 | # Fizz language documentation 2 | 3 | **Language specifics:** 4 | 5 | - [Overview](#overview) 6 | - [Grammar](#grammar) 7 | - [Types](#types) 8 | - [Keywords](#keywords) 9 | - [Expressions and operators](#expressions-and-operators) 10 | 11 | **Variables and printing:** 12 | 13 | - [Print and Type](#print-and-type) 14 | - [Error and Exit](#error-and-exit) 15 | - [Variables](#variables) 16 | - [Enums](#enums) 17 | 18 | **Conditionals and loops:** 19 | 20 | - [If statements and logic](#if-statements-and-logic) 21 | - [While loop](#while-loop) 22 | - [Repeat loop](#repeat-loop) 23 | - [Range loop](#range-loop) 24 | - [Break and skip](#break-and-skip) 25 | 26 | **Objects:** 27 | 28 | - [Functions](#functions) 29 | - [Objects](#objects) 30 | - [Arrays](#arrays) 31 | - [Reference](#reference) 32 | 33 | **Files and imports** 34 | 35 | - [File imports](#file-imports) 36 | - [Libraries](#libraries) 37 | 38 |
39 | 40 | > **IMPORTANT:** 41 | > In the examples below, `//` is used for comments since Go is used as the language for the codeblocks (for better highlighting). However, in Fizz, `#` signifies a comment. 42 | 43 |
44 | 45 | ## Overview 46 | 47 | - Fizz is dynamically typed, meaning type checks are only performed at runtime 48 | - Comments are created with a hashtag `#` (as mentioned above) and end at the first found newline 49 | - All statements that do not have a block tied to them must end with a semicolon `;`. This, of course, means you can have all your code on one line 50 | 51 |
52 | 53 | ## Grammar 54 | 55 | [A definitive grammar sheet](./grammar.md) 56 | 57 |
58 | 59 | ## Types 60 | 61 | Fizz is strongly typed, meaning unmatched types in certain expressions will cause a runtime error. 62 | 63 | - `string` Any string of text with a starting and ending quote `"` symbol. Can span over multiple lines. Can also include `\n` for a new line, or `\t` for a tab. 64 | 65 | - `number` Any number, including floats. 66 | 67 | - `nil` No value. 68 | 69 | - `bool` Keywords `true` and `false`. 70 | 71 | - **Truthyness**: Any expression that does not evaluate to `nil` or `false` is truthy. 72 | 73 | - `object` Type of object instance 74 | 75 | - `function` Type of function or object constructor 76 | 77 | - `array` Type of array instance 78 | 79 |
80 | 81 | ## Keywords 82 | 83 | Keyword names are reserved and cannot be used for variable names. Here is a list of all keywords in Fizz: 84 | 85 | ``` 86 | print type func range error 87 | exit skip break return in 88 | false nil include if enum 89 | import define true while repeat 90 | ``` 91 | 92 |
93 | 94 | ## Expressions and operators 95 | 96 | Fizz features a lot of standard syntax similar to other languages. For example, all normal expressions using the basic arithmetic and logic operators will work in Fizz, including the modulo operator and the hat operator. Plus can also be used for joining strings. 97 | 98 | Operators: 99 | 100 | - Binary operators: 101 | ```go 102 | + - * / % ^ < 103 | > == != >= <= & : in 104 | ``` 105 | - Unary operators: 106 | ```go 107 | - ! type 108 | ``` 109 | - Assignment operators: 110 | ```go 111 | = := 112 | ``` 113 | 114 |
115 | 116 | ## Print and Type 117 | 118 | In Fizz, `print` is a _statement_, not a function. However, `type` is an _operator_, not a function, and gives a string value. 119 | 120 | ```go 121 | print "Hello"; // Hello 122 | print type "World"; // string 123 | ``` 124 | 125 |
126 | 127 | ## Error and Exit 128 | 129 | The `error` statement prints out a message (or value) as an error and exits the program. 130 | 131 | ```go 132 | error "some error occured"; // prints message as error and exits 133 | ``` 134 | 135 | Theres also an `exit` statement. This will just print the value out (same as `print`) and then exit with no error. If an expression is not given, `exit` will just quit without printing anything. 136 | 137 | ```go 138 | exit "goodbye"; // prints message and exits 139 | ``` 140 | 141 |
142 | 143 | ## Variables 144 | 145 | You can declare a variable using the `:=` operator. The value can be re-assigned later and even change type. 146 | 147 | ```go 148 | name := "John"; 149 | name = "Carl"; 150 | name = 3; 151 | 152 | // Error, 'name' is already defined 153 | name := "Susan"; 154 | ``` 155 | 156 | Local variables override higher level scopes: 157 | 158 | ```go 159 | age := 10; 160 | 161 | { 162 | // Overrides global 'age' variable 163 | age := 20; 164 | } 165 | ``` 166 | 167 | You can use shorthand assignment operators too: 168 | 169 | ```go 170 | n := 1; 171 | n += 2; 172 | n -= 2; 173 | n *= 2; 174 | n /= 2; 175 | ``` 176 | 177 | You can also use the `+=` operator with strings. 178 | 179 |
180 | 181 | ## Enums 182 | 183 | Fizz also has enums to quickly create a lot of variables with unique values. 184 | 185 | ```go 186 | enum { 187 | banana 188 | apple 189 | orange 190 | } 191 | 192 | enum { 193 | pear 194 | } 195 | 196 | print banana; // 0 197 | print apple; // 1 198 | print orange; // 2 199 | print pear; // 0 200 | ``` 201 | 202 |
203 | 204 | ## If statements and logic 205 | 206 | Fizz features simple `if` and `else` statements, but not `else if`. The 'and' operator is `&` and 'or' is `:`. 207 | 208 | ```go 209 | height := 172; 210 | 211 | if height > 158.8 { 212 | print "Taller than Kevin Hart"; 213 | } else { 214 | print "Not taller than Kevin Hart"; 215 | } 216 | ``` 217 | 218 |
219 | 220 | ## While loop 221 | 222 | Fizz has a while statement similar to most other languages. If you leave the expression field empty it will just run forever. 223 | 224 | ```js 225 | while n < 10 { 226 | // loops until condition is false 227 | } 228 | 229 | while { 230 | // loops until break or program exit 231 | } 232 | ``` 233 | 234 |
235 | 236 | ## Repeat loop 237 | 238 | A repeat loop is another basic flow controller that just executes a block n times. 239 | 240 | ```go 241 | // prints "hi" 5 times 242 | repeat 5 { 243 | print "hi"; 244 | } 245 | ``` 246 | 247 |
248 | 249 | ## Range loop 250 | 251 | The range loop is a more advanced form of loop, kind of a hybrid bewteen pythons `for _ in _` statements and other languages `for` loops. The simplest use case is to just give one argument to the right side of `in`. 252 | 253 | In this case, it will just loop with `n` going from 0 to 9, as the default starting number is 0: 254 | 255 | ```go 256 | range n in 10 { 257 | print n; 258 | } 259 | ``` 260 | 261 | Providing two arguments defines both the start and end for the loop: 262 | 263 | ```go 264 | // Goes from 3 to 7 265 | range n in 3, 8 { 266 | print n; 267 | } 268 | ``` 269 | 270 | Three arguments define start, end, and iteration amount. The amount can be negative and make the loop count down, but if the conditions are set in a way where the loop will never end, an error is raised: 271 | 272 | ```go 273 | range n in 0, 5, 0.5 { 274 | print n; 275 | } 276 | 277 | // error: infinite loop not allowed for range statement 278 | range q in 0, 5, -1 { 279 | 280 | } 281 | ``` 282 | 283 | Additionally, you can range over an array: 284 | 285 | ```go 286 | days := ["Monday", "Tuesday", "Wednesday"]; 287 | 288 | range day in days { 289 | print day; 290 | } 291 | ``` 292 | 293 |
294 | 295 | ## Break and skip 296 | 297 | - `skip` skips to next iteration in loop 298 | - `break` breaks out of loop 299 | 300 |
301 | 302 | ## Functions 303 | 304 | You can declare a function using the `func` keyword. Functions will return `nil` if no other return value is specified. Passing an incorrect argument number will cause a runtime error. 305 | 306 | ```go 307 | func add(a, b) { 308 | return a + b; 309 | } 310 | 311 | print add(5, 2); // 7 312 | ``` 313 | 314 |
315 | 316 | ## Objects 317 | 318 | Object structures can be defined with the `define` keyword. This creates an object template which you can use to make your own structured data. The field names can be separated by a line break, comma, or space. The fields of the object do not have a specific type, unlike languages like C and Go. Object values support reassignment too. 319 | 320 | ```go 321 | define Person { 322 | name 323 | age 324 | } 325 | 326 | john := Person("John", 31); 327 | print john.name; // "John" 328 | 329 | john.age = 99; 330 | print john.age; // 99 331 | ``` 332 | 333 | Under the hood, the `define` statement creates a function that returns an object with the specified values. That means `Person` is a `function` type and `john` is an `object` type: 334 | 335 | ```go 336 | print type Person // function 337 | print type john // object 338 | ``` 339 | 340 |
341 | 342 | ## Arrays 343 | 344 | Arrays in Fizz are just an array of values, of which can be any type. You get the value of a specific index in an array by using the index getter syntax. Indexes begin at 0. Additionally, you can get the length of an array with the built-in `len` function. 345 | 346 | ```go 347 | names := ["John", "Susan", "Carl"]; 348 | print names[0]; // John 349 | 350 | names[2] = "Timmy"; 351 | print names; // ["John", "Susan", "Timmy"] 352 | 353 | print len(names); // 3 354 | ``` 355 | 356 | You can use the `in` operator to check if an element is present in an array: 357 | 358 | ```js 359 | print "dog" in ["cat", "dog", "fox"]; // true 360 | ``` 361 | 362 | ### Push 363 | 364 | To push elements into an array use the built-in `push` function: 365 | 366 | ```go 367 | arr := [1, 2, 3]; 368 | push(arr, 4); 369 | 370 | print arr; // [1, 2, 3, 4] 371 | ``` 372 | 373 | ### Pop 374 | 375 | There is also a `pop` function to remove and return the last element: 376 | 377 | ```go 378 | arr := [1, 2, 3]; 379 | print pop(arr); // 3 380 | print arr; // [1, 2] 381 | ``` 382 | 383 |
384 | 385 | ## Reference 386 | 387 | In Fizz, objects and arrays are passed by reference. This means you can modify them directly when passing them as a function argument: 388 | 389 | ```go 390 | food := ["bread", "pasta", "rice"]; 391 | 392 | func addCarrot(arr) { 393 | push(arr, "carrot"); 394 | } 395 | 396 | addCarrot(food); 397 | print food; // ["bread", "pasta", "rice", "carrot"] 398 | ``` 399 | 400 | ```go 401 | define Phone { 402 | brand 403 | version 404 | } 405 | 406 | iphone := Phone("Apple", 1); 407 | 408 | func upgrade(phone) { 409 | phone.version += 1; 410 | } 411 | 412 | upgrade(iphone); 413 | print iphone.version; // 2 414 | ``` 415 | 416 |
417 | 418 | ## File imports 419 | 420 | You can import files by using the `import` statement. The given path, or name, is always relative to the file that the program started in. Circular imports are not allowed and an error will be raised if one is found. Importing creates an object with all the values of the imported file. The object is declared with the name of the file that was imported, so files with the same names cannot be imported in the same file. (in the future `import x as y` syntax will be added to fix this) 421 | 422 | ```go 423 | // other.fizz 424 | 425 | name := "John"; 426 | ``` 427 | 428 | ```js 429 | // main.fizz 430 | 431 | import "other"; 432 | // Also valid: 433 | // import "other.fizz"; 434 | // import "./other.fizz"; 435 | 436 | print other.name; 437 | ``` 438 | 439 | ```console 440 | $ ls 441 | main.fizz other.fizz 442 | 443 | $ fizz main 444 | John 445 | ``` 446 | 447 |
448 | 449 | ## Libraries 450 | 451 | Fizz libraries are different from imports. They are not other Fizz files, but rather Go files. This is to make it possible for functionality to be added to Fizz without baking it straight in. You can read the [library documentation](./libraries.md) to find out how they work and how to create your own. 452 | 453 | Fizz has a standard library built in. Include it with the `include` keyword. The functions are documented in the `lib/` directory. 454 | 455 | ```go 456 | include "str"; 457 | 458 | age := 32; 459 | print "John is " + str.toString(age); // prints: John is 32 460 | // Causes no type error because age is converted to a string 461 | ``` 462 | 463 | ```go 464 | include "io"; 465 | include "str"; 466 | 467 | // Will prompt the terminal and wait for user input. 468 | // Continues at newline or exit with ctrl-C. 469 | meters := io.input("Enter height in meters: "); 470 | 471 | feet := str.toNumber(meters) * 3.281; 472 | print "You are: " + str.toString(feet) + " feet tall"; 473 | // Built-in string formatting is on the todo list, don't worry ;) 474 | ``` 475 | 476 |
477 | 478 | [Go to top](#top) 479 | -------------------------------------------------------------------------------- /docs/libraries.md: -------------------------------------------------------------------------------- 1 | # **Fizz libraries** 2 | 3 | - [Overview](#overview) 4 | - [Make your own](#making-your-own-library) 5 | - [Setup](#setup) 6 | - [Content](#content) 7 | - [Build](#building) 8 | - [Docs](#automatic-documentation) 9 | 10 |
11 | 12 | ## Overview 13 | 14 | Fizz allows for Go libraries to be included in your Fizz code. Use the `include` statement followed by the library name to use it: 15 | 16 | ```go 17 | include "io"; 18 | 19 | name := io.input("Enter name: "); 20 | ``` 21 | 22 |
23 | 24 | ## Making your own library 25 | 26 | You can easily create your own library for Fizz. The only requirements are: 27 | 28 | - A unique library name as duplicates are not allowed 29 | - That you follow the steps below to make sure your library is created properly 30 | 31 |
32 | 33 | ### Setup 34 | 35 | Create a new folder in the `lib` directory and name it after your library. Here is an example structure: 36 | 37 | ``` 38 | lib/ 39 | mylib/ 40 | main.go 41 | ``` 42 | 43 |
44 | 45 | ### Content 46 | 47 | Here is an example for a simple library with a single function; `SayHello()`. Notice how the first letter is capitilized, declaring it as an exported function. However, when used in a Fizz program it will have the first letter lowercased to follow Fizz naming conventions. 48 | 49 | ```go 50 | package mylib 51 | 52 | // All functions in a library must return a value (interface) and error. If the 53 | // error returned is not nil, it will be raised as a fizz error and terminate the 54 | // program. The value returned is the value the function returns when calling it 55 | // from Fizz, so it must be a valid Fizz type. 56 | func SayHello(name string) (val interface{}, err error) { 57 | return "Hello, " + name, err 58 | } 59 | ``` 60 | 61 | The types of the arguments, as well as the argument count, is checked before trying to call the function, so if they dont match up an error is raised. The return types for library functions are always `interface` and `error`. 62 | 63 |
64 | 65 | ## Building 66 | 67 | To add your library or update it with the new changes run either the `build.sh` script, to build a binary, or the `lib/build.py` script to just add the library. 68 | 69 | With the example library above added we can now write this simple Fizz program: 70 | 71 | ```go 72 | include "mylib"; 73 | 74 | print mylib.sayHello("John"); 75 | ``` 76 | 77 | ```console 78 | $ ls 79 | main.fizz 80 | 81 | $ fizz main 82 | Hello, John 83 | ``` 84 | 85 |
86 | 87 | ## Automatic documentation 88 | 89 | Additionally, you can make simple documentation for your library. Use the following format to add documentation to your functions: 90 | 91 | ```go 92 | /* 93 | Returns a greeting to the name given. 94 | func sayHello(name string) string 95 | */ 96 | func SayHello(name string) (val interface{}, err error) { 97 | return "Hello, " + name, err 98 | } 99 | ``` 100 | 101 | This will automatically be added to a markdown file in the same directory by running the `lib/autodocs.py` file. It will result in this: 102 | 103 |
104 | 105 | ## **`sayHello`** 106 | 107 | Returns a greeting to the name given. 108 | 109 | ```go 110 | func sayHello(name string) string 111 | ``` 112 | -------------------------------------------------------------------------------- /env/create.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | func NewObject(name string, elements map[string]interface{}) *Object { 4 | return &Object{Name: name, NumFields: len(elements), Fields: elements} 5 | } 6 | 7 | func NewFunction(name string, numArgs int, callback CallFunction) *Callable { 8 | return &Callable{Name: name, NumArgs: numArgs, Call: callback} 9 | } 10 | 11 | func NewArray(elements []interface{}) *Array { 12 | return &Array{Values: elements, Length: len(elements)} 13 | } 14 | -------------------------------------------------------------------------------- /env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | // Max number of function names printed upon error 11 | CallStackSize = 10 12 | ) 13 | 14 | var ( 15 | ThrowEnvironment = true 16 | ) 17 | 18 | type valueMap map[string]interface{} 19 | 20 | // List of 'scopes'. Index 0 is always the current scope and when looping the 21 | // order will be from low to high level scopes. 22 | type Environment []valueMap 23 | 24 | var currentEnv = CopyEnvironment(StandardEnvironment) 25 | var tempEnv = currentEnv 26 | 27 | // Callstack is slice of names/origins of functions. It is only appended to from 28 | // failing functions, and the ripple back effect from the returned errors will 29 | // fill it with the names of the failed functions. 30 | var callStack = []string{} 31 | 32 | // Appends failed function to callstack. 33 | func FailCall(name string, origin string, line int) { 34 | callStack = append(callStack, fmt.Sprintf("\tat %s() in %s, line %d", name, origin, line)) 35 | } 36 | 37 | // Get print ready format of callstack for errors. 38 | func GetCallstack() string { 39 | if len(callStack) > CallStackSize { 40 | str := strings.Join(callStack[:CallStackSize], "\n") 41 | return str + "\n\t..." 42 | } 43 | 44 | return strings.Join(callStack, "\n") 45 | } 46 | 47 | // Creates new environment, replacing the old one. Returns old environment. For 48 | // testing, its not necessary to get rid of the old env, hence the option to not 49 | // remove it. 50 | func NewEnvironment() Environment { 51 | if !ThrowEnvironment { 52 | return currentEnv 53 | } 54 | 55 | oldEnv := currentEnv 56 | currentEnv = CopyEnvironment(StandardEnvironment) 57 | return oldEnv 58 | } 59 | 60 | // Declares value to name in current scope. This allows for overriding global 61 | // variable names for local scopes. Returns error if name is already declared. 62 | func Declare(name string, value interface{}) error { 63 | curScope := currentEnv[0] 64 | if _, ok := curScope[name]; !ok { 65 | curScope[name] = value 66 | return nil 67 | } 68 | 69 | return errors.New("variable '" + name + "' is already defined, line %d") 70 | } 71 | 72 | // Assigns value to name. If name is not defined in current scope the parent 73 | // scopes are checked. Therefore, reassignment of global variables in local 74 | // scopes is possible. Returns error if name is not defined anywhere. 75 | func Assign(name string, value interface{}) error { 76 | for _, scope := range currentEnv { 77 | if _, ok := scope[name]; ok { 78 | scope[name] = value 79 | return nil 80 | } 81 | } 82 | 83 | return errors.New("undefined variable '" + name + "', line %d") 84 | } 85 | 86 | // Gets the value assigned to name. If the name is not defined in the current 87 | // scope the parent scopes are checked. Gets the first instance of name. Returns 88 | // error if name is not defined anywhere. 89 | func Get(name string) (value interface{}, err error) { 90 | for _, scope := range currentEnv { 91 | if value, ok := scope[name]; ok { 92 | return value, nil 93 | } 94 | } 95 | 96 | return value, errors.New("undefined variable '" + name + "', line %d") 97 | } 98 | 99 | // Puts new scope at beginning of slice, effectivly setting the previous scope 100 | // as the parent of the new. (slice is reverse stack) 101 | func PushScope() { 102 | currentEnv = append([]valueMap{{}}, currentEnv...) 103 | } 104 | 105 | // Removes first element in slice, meaning the parent scope is set to the current. 106 | // Unsafe: if the length of the slice is 1, pop will panic. However, the use of Push() 107 | // and Pop() is hardcoded and will never cause a pop of a scope list smaller than 2. 108 | func PopScope() { 109 | if len(currentEnv) < 2 { 110 | panic("env: popped scope list of length < 2") 111 | } 112 | 113 | currentEnv = currentEnv[1:] 114 | } 115 | 116 | // Adds environment from imported file to current env. Values are passed as an object. 117 | // The fields are the values in the global scope of the environment (index 0). 118 | func AddImportedFile(name string, env Environment) error { 119 | return Declare(name, &Object{ 120 | Name: name, 121 | NumFields: len(env[0]), 122 | Fields: env[0], 123 | }) 124 | } 125 | 126 | // Gets a snapshot of the current env. Used to disallow changes made to the current 127 | // env to be accessed in closures. More static approach. Unsafe: gets temp env if 128 | // one is in use. 129 | func GetCurrentEnvSnapshot() Environment { 130 | temp := Environment{} 131 | for idx, m := range currentEnv { 132 | temp = append(temp, valueMap{}) 133 | for k, v := range m { 134 | temp[idx][k] = v 135 | } 136 | } 137 | 138 | return temp 139 | } 140 | 141 | // Returns the current env. Used instead of GetCurrentEnvSnapshot to allow for the 142 | // enviroment changes to be available inside a closure. 143 | func GetCurrentEnv() Environment { 144 | return currentEnv 145 | } 146 | 147 | // Sets a new temporary envirnoment. Used for closures since envs are not passed as 148 | // arguments to any functions in this file. Is discarded upon calling PopTempEnv(). 149 | func PushTempEnv(env Environment) { 150 | tempEnv = currentEnv 151 | currentEnv = env 152 | } 153 | 154 | // Unsafe: does not check if there is a current temp env or not, however, its use is 155 | // hardcoded and will not be called when there is no temporary environment. 156 | func PopTempEnv() { 157 | currentEnv = tempEnv 158 | } 159 | 160 | // Copies environment to not use a reference of the old one. 161 | func CopyEnvironment(env Environment) Environment { 162 | temp := Environment{{}} 163 | for k, v := range env[0] { 164 | temp[0][k] = v 165 | } 166 | 167 | return temp 168 | } 169 | -------------------------------------------------------------------------------- /env/native.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | var StandardEnvironment = Environment{{ 4 | "len": NewFunction("len", 1, func(i ...interface{}) (interface{}, error) { 5 | if arr, ok := i[0].(*Array); ok { 6 | return float64(arr.Length), nil 7 | } 8 | 9 | // Strings too 10 | if str, ok := i[0].(string); ok { 11 | return float64(len(str)), nil 12 | } 13 | 14 | return -1, ErrNotArray 15 | }), 16 | 17 | "push": NewFunction("push", 2, func(i ...interface{}) (interface{}, error) { 18 | if arr, ok := i[0].(*Array); ok { 19 | arr.Push(i[1]) 20 | return nil, nil 21 | } 22 | 23 | return -1, ErrNotArray 24 | }), 25 | 26 | "pop": NewFunction("pop", 1, func(i ...interface{}) (interface{}, error) { 27 | if arr, ok := i[0].(*Array); ok { 28 | return arr.Pop() 29 | } 30 | 31 | return -1, ErrNotArray 32 | }), 33 | }} 34 | -------------------------------------------------------------------------------- /env/types.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrNotAField = errors.New("'%s' has no attribute '%s', line %d") 9 | ErrIndexOutOfRange = errors.New("index out of range, line %d") 10 | ErrNotArray = errors.New("type %s is not an array, line %d") 11 | ErrEmptyArray = errors.New("cannot pop from empty array, line %d") 12 | ) 13 | 14 | // Performs recursive equality check for objects and arrays. 15 | // Other cases returns standard equality check. 16 | func Equal(left, right interface{}) bool { 17 | l, r := "", "" 18 | if lo, ok := left.(FizzObject); ok { 19 | l = lo.Type() 20 | } 21 | 22 | if ro, ok := right.(FizzObject); ok { 23 | r = ro.Type() 24 | } 25 | 26 | if l == "array" && r == "array" { 27 | a, _ := left.(*Array) 28 | b, _ := right.(*Array) 29 | return a.IsEqual(b) 30 | } 31 | 32 | if l == "object" && r == "object" { 33 | a, _ := left.(*Object) 34 | b, _ := right.(*Object) 35 | return a.IsEqual(b) 36 | } 37 | 38 | return left == right 39 | } 40 | 41 | // Interface matches all Fizz object structs. 42 | // Type() returns the name of the object 43 | type FizzObject interface { 44 | Type() string 45 | } 46 | 47 | type CallFunction func(...interface{}) (interface{}, error) 48 | 49 | // Callable object is a function. The origin is the name of the file it 50 | // was defined in. Error returned from Call() is printed as a Fizz error 51 | // and is not a return value. 52 | type Callable struct { 53 | Name string 54 | Origin string 55 | Call CallFunction 56 | NumArgs int 57 | } 58 | 59 | func (c *Callable) Type() string { 60 | return "function" 61 | } 62 | 63 | // Object with n fields. Name is the name of the constructor, not the 64 | // instance. File imports are also objects. 65 | type Object struct { 66 | Fields map[string]interface{} 67 | NumFields int 68 | Name string 69 | } 70 | 71 | func (o *Object) Type() string { 72 | return "object" 73 | } 74 | 75 | // Equality check for objects 76 | func (o *Object) IsEqual(a *Object) bool { 77 | if o.NumFields != a.NumFields { 78 | return false 79 | } 80 | 81 | for k, v := range o.Fields { 82 | ov, err := a.Get(k) 83 | if err != nil { 84 | return false 85 | } 86 | 87 | if !Equal(v, ov) { 88 | return false 89 | } 90 | } 91 | 92 | return true 93 | } 94 | 95 | // Gets value from object. Used for getter syntax "name.value" 96 | func (o *Object) Get(name string) (value interface{}, err error) { 97 | if val, ok := o.Fields[name]; ok { 98 | return val, err 99 | } 100 | 101 | return value, ErrNotAField 102 | } 103 | 104 | // Reassigns value to object. Does not declare since object have a 105 | // constant number of fields. Used for setter syntax "name.value = n" 106 | func (o *Object) Set(name string, value interface{}) (err error) { 107 | if _, ok := o.Fields[name]; ok { 108 | o.Fields[name] = value 109 | return err 110 | } 111 | 112 | return ErrNotAField 113 | } 114 | 115 | // Stores length value for ease of use. Append elements with += operator. 116 | type Array struct { 117 | Values []interface{} 118 | Length int 119 | } 120 | 121 | func (a Array) Type() string { 122 | return "array" 123 | } 124 | 125 | // Compare two arrays 126 | func (a *Array) IsEqual(o *Array) bool { 127 | if a.Length != o.Length { 128 | return false 129 | } 130 | 131 | for i, n := range a.Values { 132 | if !Equal(o.Values[i], n) { 133 | return false 134 | } 135 | } 136 | 137 | return true 138 | } 139 | 140 | // Gets value of array at index. Returns error if value is > len(arr) or 141 | // index is less than 0. 142 | func (a Array) Get(index int) (value interface{}, err error) { 143 | if index >= len(a.Values) || index < 0 { 144 | return value, ErrIndexOutOfRange 145 | } 146 | 147 | return a.Values[index], err 148 | } 149 | 150 | // Sets value at given index. 151 | func (a Array) Set(index int, value interface{}) error { 152 | if index >= a.Length || index < 0 { 153 | return ErrIndexOutOfRange 154 | } 155 | 156 | a.Values[index] = value 157 | return nil 158 | } 159 | 160 | // Pushes new value to end of array 161 | func (a *Array) Push(value interface{}) { 162 | a.Values = append(a.Values, value) 163 | a.Length++ 164 | } 165 | 166 | // Removes value at end of array and returns it. Returns error if 167 | // length of array is 0. 168 | func (a *Array) Pop() (value interface{}, err error) { 169 | if a.Length == 0 { 170 | return nil, ErrEmptyArray 171 | } 172 | 173 | popped, _ := a.Get(a.Length - 1) 174 | a.Values = a.Values[:a.Length-1] 175 | a.Length-- 176 | return popped, nil 177 | } 178 | -------------------------------------------------------------------------------- /examples/linked_list.fizz: -------------------------------------------------------------------------------- 1 | # Linked list example 2 | 3 | include "str"; 4 | 5 | define Node { 6 | next 7 | value 8 | } 9 | 10 | func newList(value) { 11 | return Node(nil, value); 12 | } 13 | 14 | func add(head, value) { 15 | if !head.next { 16 | head.next = Node(nil, value); 17 | return; 18 | } 19 | 20 | add(head.next, value); 21 | } 22 | 23 | func printList(head) { 24 | t := ""; 25 | current := head; 26 | while { 27 | t += str.toString(current.value) + " -> "; 28 | if !current.next { 29 | break; 30 | } 31 | 32 | current = current.next; 33 | } 34 | 35 | return t; 36 | } 37 | 38 | a := newList(10); 39 | add(a, 20); 40 | add(a, 30); 41 | 42 | print printList(a); # 10 -> 20 -> 30 -> -------------------------------------------------------------------------------- /examples/number_guesser.fizz: -------------------------------------------------------------------------------- 1 | include "io"; 2 | include "math"; 3 | include "str"; 4 | 5 | # Number guessing game example 6 | 7 | number := math.floor(math.random() * 10); 8 | 9 | while { 10 | input := io.input("Enter guess: "); 11 | guess := str.toNumber(input); 12 | 13 | if guess == number { 14 | exit "You win!"; 15 | } 16 | 17 | if guess > number { 18 | print "Too high"; 19 | } else { 20 | print "Too low"; 21 | } 22 | } -------------------------------------------------------------------------------- /examples/user_database/entries.fizz: -------------------------------------------------------------------------------- 1 | include "io"; 2 | 3 | filename := "users.txt"; 4 | 5 | func createEntry(item) { 6 | if !io.exists(filename) { 7 | io.newFile(filename); 8 | } 9 | 10 | io.appendFile(filename, item); 11 | } 12 | -------------------------------------------------------------------------------- /examples/user_database/user.fizz: -------------------------------------------------------------------------------- 1 | include "str"; 2 | include "io"; 3 | 4 | import "entries"; 5 | 6 | define User { 7 | name 8 | age 9 | } 10 | 11 | func createNewUser() { 12 | name := io.input("Enter name: "); 13 | age := io.input("Enter age: "); 14 | 15 | newUser := User(name, str.toNumber(age)); 16 | return newUser; 17 | } 18 | 19 | user := createNewUser(); 20 | entries.createEntry(str.format(user)); -------------------------------------------------------------------------------- /examples/user_database/users.txt: -------------------------------------------------------------------------------- 1 | User: { 2 | age: 31 3 | name: john 4 | } -------------------------------------------------------------------------------- /expr/evaluate.go: -------------------------------------------------------------------------------- 1 | package expr 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strings" 7 | 8 | "github.com/jesperkha/Fizz/env" 9 | "github.com/jesperkha/Fizz/lexer" 10 | "github.com/jesperkha/Fizz/util" 11 | ) 12 | 13 | // Evaluates expression tree. Hands off to helper methods which can also recursively call to 14 | // resolve nested expressions. Returned value is result of expression and is Go literal. 15 | func EvaluateExpression(expr *Expression) (value interface{}, err error) { 16 | switch expr.Type { 17 | case Literal: 18 | return evalLiteral(expr) 19 | case Unary: 20 | return evalUnary(expr) 21 | case Binary: 22 | return evalBinary(expr) 23 | case Group: 24 | return EvaluateExpression(expr.Inner) 25 | case Variable: 26 | return env.Get(expr.Name) 27 | case Call: 28 | return evalCall(expr) 29 | case Getter: 30 | return evalGetter(expr) 31 | case Array: 32 | return evalArray(expr) 33 | case Index: 34 | return evalIndex(expr) 35 | } 36 | 37 | // Wont be reached 38 | return expr, ErrInvalidExpression 39 | } 40 | 41 | // Performs equality check and parsing for arrays and object 42 | // because they are pointers and cannot be compared as addresses. 43 | func equal(left, right interface{}) bool { 44 | l, r := util.GetType(left), util.GetType(right) 45 | 46 | if l == "array" && r == "array" { 47 | a, _ := left.(*env.Array) 48 | b, _ := right.(*env.Array) 49 | return a.IsEqual(b) 50 | } 51 | 52 | if l == "object" && r == "object" { 53 | a, _ := left.(*env.Object) 54 | b, _ := right.(*env.Object) 55 | return a.IsEqual(b) 56 | } 57 | 58 | return left == right 59 | } 60 | 61 | // Token types >= string are valid literal types 62 | func evalLiteral(literal *Expression) (value interface{}, err error) { 63 | if literal.Value.Type >= lexer.STRING { 64 | return literal.Value.Literal, err 65 | } 66 | 67 | return value, ErrInvalidExpression 68 | } 69 | 70 | func evalUnary(unary *Expression) (value interface{}, err error) { 71 | right, err := EvaluateExpression(unary.Right) 72 | if err != nil { 73 | return value, err 74 | } 75 | 76 | // Matches to operator 77 | switch unary.Operand.Type { 78 | case lexer.MINUS: 79 | if isNumber(right) { 80 | return -right.(float64), err 81 | } 82 | op, typ, line := unary.Operand.Lexeme, util.GetType(right), unary.Line 83 | return nil, fmt.Errorf(ErrInvalidOperatorType.Error(), op, typ, line) 84 | case lexer.NOT: 85 | return !isTruthy(right), err 86 | case lexer.TYPE: 87 | return util.GetType(right), err 88 | } 89 | 90 | // If none of the mentioned operators are present its an invalid one 91 | op, line := unary.Operand.Lexeme, unary.Line 92 | return value, fmt.Errorf(ErrInvalidUnaryOperator.Error(), op, line) 93 | } 94 | 95 | func evalBinary(binary *Expression) (value interface{}, err error) { 96 | opType := binary.Operand.Type 97 | 98 | // Recursivly evaluates left and right expressions 99 | left, err := EvaluateExpression(binary.Left) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | right, err := EvaluateExpression(binary.Right) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | // Operations if both are number types 110 | if isNumber(right) && isNumber(left) { 111 | vl, vr := left.(float64), right.(float64) 112 | switch opType { 113 | case lexer.PLUS: 114 | return vl + vr, err 115 | case lexer.MINUS: 116 | return vl - vr, err 117 | case lexer.STAR: 118 | return vl * vr, err 119 | case lexer.HAT: 120 | return math.Pow(vl, vr), err 121 | case lexer.GREATER: 122 | return vl > vr, err 123 | case lexer.LESS: 124 | return vl < vr, err 125 | case lexer.LESS_EQUAL: 126 | return vl <= vr, err 127 | case lexer.GREATER_EQUAL: 128 | return vl >= vr, err 129 | case lexer.MODULO: 130 | return float64(int(vl) % int(vr)), err 131 | case lexer.SLASH: 132 | if vr == 0 { 133 | return nil, util.FormatError(ErrDivideByZero, binary.Line) 134 | } 135 | return vl / vr, err 136 | } 137 | } 138 | 139 | // Types do not need to match for comparisons 140 | switch opType { 141 | case lexer.EQUAL_EQUAL: 142 | return equal(left, right), err 143 | case lexer.NOT_EQUAL: 144 | return !equal(left, right), err 145 | case lexer.AND: 146 | return isTruthy(left) && isTruthy(right), err 147 | case lexer.OR: 148 | return isTruthy(left) || isTruthy(right), err 149 | } 150 | 151 | // Support string addition 152 | if util.GetType(left) == "string" && util.GetType(right) == "string" && opType == lexer.PLUS { 153 | return strings.Join([]string{left.(string), right.(string)}, ""), err 154 | } 155 | 156 | // Binary 'in' operator for arrays 157 | if util.GetType(right) == "array" && opType == lexer.IN { 158 | arr, _ := right.(*env.Array) 159 | for _, v := range arr.Values { 160 | if equal(v, left) { 161 | return true, err 162 | } 163 | } 164 | 165 | return false, err 166 | } 167 | 168 | // If non of the previous checks worked the expression is invalid 169 | typeLeft, typeRight := util.GetType(left), util.GetType(right) 170 | op, line := binary.Operand.Lexeme, binary.Line 171 | return nil, fmt.Errorf(ErrInvalidOperatorTypes.Error(), op, typeLeft, typeRight, line) 172 | } 173 | 174 | func evalCall(call *Expression) (value interface{}, err error) { 175 | callee, err := EvaluateExpression(call.Left) 176 | if err != nil { 177 | return value, err 178 | } 179 | 180 | // Function should be of type env.Callable 181 | if f, ok := callee.(*env.Callable); ok { 182 | argToken := call.Inner.Inner 183 | args := []interface{}{} 184 | 185 | // Single argument 186 | if argToken.Type != Args && argToken.Type != EmptyExpression { 187 | arg, err := EvaluateExpression(call.Inner) 188 | if err != nil { 189 | return value, err 190 | } 191 | 192 | args = append(args, arg) 193 | } 194 | 195 | // Argument list 196 | if argToken.Type == Args { 197 | for _, arg := range argToken.Exprs { 198 | val, err := EvaluateExpression(&arg) 199 | if err != nil { 200 | return value, err 201 | } 202 | 203 | args = append(args, val) 204 | } 205 | } 206 | 207 | // -1 is set from /lib and should be ignored as it is handled there 208 | if len(args) != f.NumArgs && f.NumArgs != -1 { 209 | return value, fmt.Errorf(ErrIncorrectArgs.Error(), f.Name, f.NumArgs, len(args), call.Line) 210 | } 211 | 212 | // Errors from lib need line format 213 | value, err = f.Call(args...) 214 | return value, util.FormatError(err, call.Line) 215 | } 216 | 217 | return value, fmt.Errorf(ErrNotFunction.Error(), util.GetType(callee), call.Line) 218 | } 219 | 220 | func evalGetter(getter *Expression) (value interface{}, err error) { 221 | line := getter.Line 222 | name := getter.Right.Name 223 | // No name before dot raises error here. No name after dot raises error in lexer. 224 | if getter.Left.Type == EmptyExpression { 225 | return value, fmt.Errorf(ErrInvalidExpression.Error(), line) 226 | } 227 | 228 | // Recursively get parent expression, must be object 229 | parent, err := EvaluateExpression(getter.Left) 230 | if err != nil { 231 | return value, err 232 | } 233 | 234 | // Only objects allow getter expressions 235 | if obj, ok := parent.(*env.Object); ok { 236 | value, err = obj.Get(name) 237 | if err != nil { 238 | return value, fmt.Errorf(err.Error(), obj.Name, name, line) 239 | } 240 | 241 | return value, err 242 | } 243 | 244 | return value, fmt.Errorf(ErrNotObject.Error(), util.GetType(parent), line) 245 | } 246 | 247 | func evalArray(array *Expression) (value interface{}, err error) { 248 | inner := array.Inner 249 | if inner.Type == EmptyExpression { 250 | return &env.Array{}, err 251 | } 252 | 253 | values := []interface{}{} 254 | // Single argument 255 | if inner.Type != Args && inner.Type != EmptyExpression { 256 | v, err := EvaluateExpression(inner) 257 | if err != nil { 258 | return value, err 259 | } 260 | 261 | values = append(values, v) 262 | } 263 | 264 | // Multiple arguments 265 | if inner.Type == Args { 266 | for _, expr := range inner.Exprs { 267 | v, err := EvaluateExpression(&expr) 268 | if err != nil { 269 | return value, err 270 | } 271 | 272 | values = append(values, v) 273 | } 274 | } 275 | 276 | return &env.Array{Values: values, Length: len(values)}, err 277 | } 278 | 279 | func evalIndex(array *Expression) (value interface{}, err error) { 280 | line := array.Line 281 | arr, err := EvaluateExpression(array.Left) 282 | if err != nil { 283 | return value, err 284 | } 285 | 286 | index, err := EvaluateExpression(array.Right) 287 | if err != nil { 288 | return value, err 289 | } 290 | 291 | // Get index as integer. If not return error 292 | indexInt, ok := util.IsInt(index) 293 | if !ok { 294 | return value, fmt.Errorf(ErrNotInteger.Error(), line) 295 | } 296 | 297 | if a, ok := arr.(*env.Array); ok { 298 | // Env handles getting index and errors for out of range etc 299 | value, err = a.Get(indexInt) 300 | if err != nil { 301 | return value, fmt.Errorf(err.Error(), line) 302 | } 303 | 304 | return value, err 305 | } 306 | 307 | // Get string index 308 | if s, ok := arr.(string); ok { 309 | if indexInt > len(s) { 310 | return value, env.ErrIndexOutOfRange 311 | } 312 | 313 | return string(s[indexInt]), err 314 | } 315 | 316 | // arr is not array (or string) 317 | return value, fmt.Errorf(env.ErrNotArray.Error(), util.GetType(arr), line) 318 | } 319 | 320 | func isTruthy(value interface{}) bool { 321 | return value != false && value != nil 322 | } 323 | 324 | func isNumber(value interface{}) bool { 325 | return util.GetType(value) == "number" 326 | } 327 | -------------------------------------------------------------------------------- /expr/expr.go: -------------------------------------------------------------------------------- 1 | package expr 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/jesperkha/Fizz/lexer" 7 | ) 8 | 9 | var ( 10 | ErrParenError = errors.New("unmatched parenthesies, line %d") 11 | ErrBracketError = errors.New("unmatched brackets, line %d") 12 | ErrInvalidUnaryOperator = errors.New("invalid unary operator '%s', line %d") 13 | ErrInvalidOperatorType = errors.New("invalid operator '%s' for type %s, line %d") 14 | ErrInvalidOperatorTypes = errors.New("invalid operator '%s' for types %s and %s, line %d") 15 | ErrDivideByZero = errors.New("division by 0, line %d") 16 | ErrNoExpression = errors.New("empty expression, line %d") 17 | ErrInvalidExpression = errors.New("invalid expression, line %d") 18 | ErrExpectedExpression = errors.New("expected expression in group, line %d") 19 | ErrNotInteger = errors.New("index must be integer, line %d") 20 | ErrCommaError = errors.New("comma error, line %d") 21 | ErrIncorrectArgs = errors.New("%s() expected %d args, got %d, line %d") 22 | ErrNotFunction = errors.New("type %s is not a function, line %d") 23 | ErrNilValueError = errors.New("unexpected nil value in expression, line %d") 24 | ErrNotObject = errors.New("type %s has no attributes, line %d") 25 | ErrInvalidType = errors.New("expr: unknown expression type, line %d") 26 | ErrExpectedName = errors.New("expected name after dot, line %d") 27 | ErrIllegalType = errors.New("unknown type '%s'") 28 | ) 29 | 30 | const ( 31 | EmptyExpression = iota 32 | Literal 33 | Unary 34 | Binary 35 | Group 36 | Variable 37 | Call 38 | Args 39 | Getter 40 | Array 41 | Index 42 | ) 43 | 44 | type Expression struct { 45 | Type int 46 | Line int 47 | Name string 48 | Operand lexer.Token 49 | Value lexer.Token 50 | Left *Expression 51 | Right *Expression 52 | Inner *Expression 53 | Exprs []Expression 54 | } 55 | -------------------------------------------------------------------------------- /expr/parse.go: -------------------------------------------------------------------------------- 1 | package expr 2 | 3 | import ( 4 | "github.com/jesperkha/Fizz/lexer" 5 | "github.com/jesperkha/Fizz/util" 6 | ) 7 | 8 | func ParseExpression(tokens []lexer.Token) (expr Expression, err error) { 9 | if len(tokens) == 0 { 10 | return Expression{Type: EmptyExpression}, err 11 | } 12 | 13 | // Safeguard against unmatched parens/brackets later on. This means the bracket seek functions dont 14 | // need to check for eof (unless they are seek in a sliced off token list) 15 | paren, bracket := 0, 0 16 | for _, t := range tokens { 17 | if bracket == 0 { 18 | switch t.Type { 19 | case lexer.LEFT_PAREN: 20 | paren++ 21 | case lexer.RIGHT_PAREN: 22 | paren-- 23 | } 24 | } 25 | 26 | if paren == 0 { 27 | switch t.Type { 28 | case lexer.LEFT_SQUARE: 29 | bracket++ 30 | case lexer.RIGHT_SQUARE: 31 | bracket-- 32 | } 33 | } 34 | } 35 | 36 | if paren != 0 { 37 | return expr, ErrParenError 38 | } else if bracket != 0 { 39 | return expr, ErrBracketError 40 | } 41 | 42 | line := tokens[0].Line 43 | 44 | if len(tokens) == 1 { 45 | // VARIABLE 46 | // Variables have a different expression type 47 | if tokens[0].Type == lexer.IDENTIFIER { 48 | return Expression{Type: Variable, Name: tokens[0].Lexeme, Line: line}, err 49 | } 50 | 51 | // LITERAL 52 | // Only other option is a literal, the only error this can cause is an undefined variable 53 | return Expression{Type: Literal, Value: tokens[0], Line: line}, err 54 | } 55 | 56 | // ARGUMENTS 57 | // Argument list for function calls. If the arg list is empty it is handled as an empty group. 58 | if splits := util.SplitByToken(tokens, lexer.COMMA); len(splits) != 1 { 59 | args := []Expression{} 60 | for _, list := range splits { 61 | arg, err := ParseExpression(list) 62 | if err != nil { 63 | return expr, err 64 | } 65 | args = append(args, arg) 66 | } 67 | 68 | return Expression{Type: Args, Exprs: args, Line: line}, err 69 | } 70 | 71 | // UNARY 72 | // Check if first token is a valid unary token type 73 | unaryOperators := []int{lexer.MINUS, lexer.TYPE, lexer.NOT} 74 | if util.Contains(unaryOperators, tokens[0].Type) && (len(tokens) == 2 || tokens[1].Type == lexer.LEFT_PAREN) { 75 | right, err := ParseExpression(tokens[1:]) 76 | return Expression{Type: Unary, Right: &right, Operand: tokens[0], Line: line}, err 77 | } 78 | 79 | // BINARY 80 | // Find lowest precedense operator in token list. Split at that token and put into expression tree. 81 | // Only check if not in a group. Then check if valid operator, skip if not. 82 | lowest, lowestIdx := lexer.Token{Type: 999}, 0 83 | util.SeekBreakPoint(tokens, func(i int, t lexer.Token) bool { 84 | // Checks if last token as the same, (1 - -1), and ignores if so. This means the target will 85 | // always be the last splittable token: (10 / 4 * 2) splits at * 86 | if t.Type <= lowest.Type && i != 0 && tokens[i-1].Type != t.Type { 87 | lowest, lowestIdx = t, i 88 | } 89 | return false 90 | }) 91 | 92 | t := lowest.Type 93 | if t >= lexer.AND && t <= lexer.HAT { 94 | left, err := ParseExpression(tokens[:lowestIdx]) 95 | if err != nil { 96 | return expr, err 97 | } 98 | 99 | right, err := ParseExpression(tokens[lowestIdx+1:]) 100 | return Expression{Type: Binary, Left: &left, Right: &right, Operand: tokens[lowestIdx], Line: line}, err 101 | } 102 | 103 | // GROUP 104 | // Binary is already parsed so there can either be a single group or a chained call / getter. 105 | // First gets the endIdx of the first group. Its a single group expression is the closing paren 106 | // is the last token in the list. Paren error if eof, doesnt matter how many calls come after. 107 | endIdx, _ := util.SeekClosingBracket(tokens, 0, lexer.LEFT_PAREN, lexer.RIGHT_PAREN) 108 | if tokens[0].Type == lexer.LEFT_PAREN && endIdx == len(tokens)-1 { 109 | inner, err := ParseExpression(tokens[1 : len(tokens)-1]) 110 | return Expression{Type: Group, Inner: &inner, Line: line}, err 111 | } 112 | 113 | // ARRAY LITERAL 114 | // Same as group parsing but with square bracket. 115 | endIdx, _ = util.SeekClosingBracket(tokens, 0, lexer.LEFT_SQUARE, lexer.RIGHT_SQUARE) 116 | if tokens[0].Type == lexer.LEFT_SQUARE && endIdx == len(tokens)-1 { 117 | inner, err := ParseExpression(tokens[1 : len(tokens)-1]) 118 | return Expression{Type: Array, Inner: &inner, Line: line}, err 119 | } 120 | 121 | // ARRAY GETTER 122 | // Array index getter has same priority as call 123 | targetIndex, eofIndex := util.SeekBreakPoint(tokens, func(i int, t lexer.Token) bool { 124 | return t.Type == lexer.LEFT_SQUARE 125 | }) 126 | 127 | // Also check where the closest dot is. If a dot comes after the last call, it should be parsed as a getter. 128 | // The only other option is for the call to be last (or error). 129 | targetDot, eofDot := util.SeekBreakPoint(tokens, func(i int, t lexer.Token) bool { 130 | return t.Type == lexer.DOT 131 | }) 132 | 133 | if !eofIndex && targetIndex > targetDot { 134 | array, err := ParseExpression(tokens[:targetIndex]) 135 | if err != nil { 136 | return expr, err 137 | } 138 | 139 | endIdx, _ := util.SeekClosingBracket(tokens, targetIndex, lexer.LEFT_SQUARE, lexer.RIGHT_SQUARE) 140 | arg, err := ParseExpression(tokens[targetIndex+1 : endIdx]) 141 | return Expression{Type: Index, Left: &array, Right: &arg, Line: line}, err 142 | } 143 | 144 | // FUNCTION CALL 145 | // Search for left paren, then parse the left part of the expression. Also parse the args of the caller. 146 | // Start cannot be set to 0 in loop because that would be a group expression 147 | targetCall, eofCall := util.SeekBreakPoint(tokens, func(i int, t lexer.Token) bool { 148 | return t.Type == lexer.LEFT_PAREN 149 | }) 150 | 151 | if !eofCall && targetCall > targetDot { 152 | callee, err := ParseExpression(tokens[:targetCall]) 153 | if err != nil { 154 | return expr, err 155 | } 156 | 157 | args, err := ParseExpression(tokens[targetCall:]) 158 | return Expression{Type: Call, Left: &callee, Inner: &args, Line: line}, err 159 | } 160 | 161 | // OBJECT GETTER 162 | // Splits by dot and parses the left side recursively 163 | if !eofDot { 164 | left, err := ParseExpression(tokens[:targetDot]) 165 | if err != nil { 166 | return expr, err 167 | } 168 | 169 | right, err := ParseExpression(tokens[targetDot+1:]) 170 | if right.Type != Variable { 171 | return expr, ErrExpectedName 172 | } 173 | 174 | return Expression{Type: Getter, Left: &left, Right: &right, Line: line}, err 175 | } 176 | 177 | return expr, ErrInvalidExpression 178 | } 179 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jesperkha/Fizz 2 | 3 | go 1.16 4 | 5 | require github.com/daviddengcn/go-colortext v1.0.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE= 2 | github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c= 3 | github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= 4 | github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A= 5 | github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE= 6 | github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrSEUQ= 7 | github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA= 8 | -------------------------------------------------------------------------------- /interp/interp.go: -------------------------------------------------------------------------------- 1 | package interp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/jesperkha/Fizz/env" 10 | "github.com/jesperkha/Fizz/lexer" 11 | "github.com/jesperkha/Fizz/lib" 12 | "github.com/jesperkha/Fizz/stmt" 13 | "github.com/jesperkha/Fizz/util" 14 | ) 15 | 16 | var ( 17 | ErrFileNotFound = errors.New("cannot find file with name: '%s'") 18 | ErrNonFizzFile = errors.New("cannot run non-Fizz file") 19 | ErrCircularImport = errors.New("circular import not allowed, %s <-> %s") 20 | ) 21 | 22 | // Stores import pairs to check for import cycles. Duplicate erntries indicate 23 | // a circular import and an error is raised. 24 | var importPairs = util.UniquePairs{} 25 | 26 | // Interperates string of code. The string is tokenized in the lexer package 27 | // where each token has a type, line, lexeme, and literal value. The parsed 28 | // tokens are passed to the statement parser which looks for statements 29 | // (identified by trailing semicolon or curly braces). 30 | 31 | // The statements are parsed as statement tokens with a type, name, and 32 | // expression values for the expression associated with the statement. For 33 | // example: a variable declaration statement has an expression property for 34 | // the initial value that is found after the equal sign. 35 | 36 | // The statements are then executed and, when doing so, statement expressions 37 | // are evaluated. Variable values are also assigned and manipulated in the 38 | // variable environment found in the env package. 39 | 40 | func Interperate(filename string, input string) (e env.Environment, err error) { 41 | // Parses input characters into lexical tokens for single and double symbols, 42 | // identifiers, and keywords. 43 | lexicalTokens, err := lexer.GetTokens(input) 44 | if err != nil { 45 | return e, err 46 | } 47 | 48 | // Lexical tokens are analysed and put into statement tokens. These statements 49 | // contain all the information they need for execution and error handling. 50 | statements, err := stmt.ParseStatements(lexicalTokens) 51 | if err != nil { 52 | return e, err 53 | } 54 | 55 | // File imports are handled after parsing the statements, not when executing them. 56 | // This means that all imports are run before anything else; they are "hoisted". 57 | // Even declaring a variable with the same name as the file before importing it will 58 | // raise an error as the file is imported before the variable is created. 59 | includes := []string{} 60 | for _, s := range statements { 61 | if s.Type == stmt.Include { 62 | includes = append(includes, s.Name) 63 | } 64 | 65 | if s.Type != stmt.Import { 66 | continue 67 | } 68 | 69 | // Checks for circular imports. Add() returns true if the pair already exists. 70 | name := util.GetPlainFilename(s.Name) 71 | if this := util.GetPlainFilename(filename); importPairs.Add(name, this) { 72 | return e, fmt.Errorf(ErrCircularImport.Error(), name, this) 73 | } 74 | 75 | e, err = RunFile(s.Name + ".fizz") 76 | if err != nil { 77 | return e, err 78 | } 79 | 80 | // Adds the global environment of the imported file to the env of the current one. 81 | // It is added as an object instance with the name of the file without the fizz suffix. 82 | if err = env.AddImportedFile(name, e); err != nil { 83 | return e, err 84 | } 85 | } 86 | 87 | // Include the mentioned libraries in this file. Returns error if names are not 88 | // valid library names. The lib package parses the Go functions into Fizz callables. 89 | err = lib.IncludeLibraries(includes) 90 | if err != nil { 91 | return e, err 92 | } 93 | 94 | // Set origin point for function declarations. This makes sure that errors give 95 | // the correct filename when printed. 96 | stmt.CurrentOrigin = filename 97 | 98 | // Finally executes statement tokens. This is the only step that has any effect 99 | // on the actual input program as the others were just breaking it up into usable 100 | // pieces. While the interpreter is still running, the values of variables will be 101 | // remembered as the environments are never reset at runtime. 102 | err = stmt.ExecuteStatements(statements) 103 | return env.NewEnvironment(), err 104 | } 105 | 106 | // Runs a fizz file. Imports are run as files and the environment is extracted and 107 | // packaged into a namespace. Said namespace is put into the file it was imported from, 108 | // which means if "main.fizz" imports "other.fizz", the main file also imports all 109 | // of the files imported in "other.fizz". 110 | func RunFile(filename string) (e env.Environment, err error) { 111 | if !strings.Contains(filename, ".") { 112 | filename = filename + ".fizz" 113 | } 114 | 115 | if !strings.HasSuffix(filename, ".fizz") { 116 | return e, ErrNonFizzFile 117 | } 118 | 119 | if byt, err := os.ReadFile(filename); err == nil { 120 | e, err = Interperate(filename, string(byt)) 121 | return e, util.WrapFilename(filename, err) 122 | } 123 | 124 | // Unsafe: assumes path error 125 | return e, fmt.Errorf(ErrFileNotFound.Error(), filename) 126 | } 127 | -------------------------------------------------------------------------------- /lexer/lexer.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | // Tokenizes text input into slice. Errors can be raised 4 | // for invalid tokens, identifiers, or unlosed strings. 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | ErrUnexpectedToken = errors.New("unexpeted token: '%s', line: %d") 16 | ErrUnterminatedString = errors.New("unterminated string, line %d") 17 | ErrInvalidSyntax = errors.New("invalid syntax '%s', line %d") 18 | ) 19 | 20 | type Token struct { 21 | Type int 22 | Lexeme string 23 | Literal interface{} 24 | Line int 25 | } 26 | 27 | func GetTokens(input string) (tokens []Token, err error) { 28 | currentIdx := 0 29 | currentLine := 1 // Start at line 1 for editors 30 | alphaNumRegex := regexp.MustCompile("^[a-zA-Z_0-9.]*$") 31 | variableRegex := regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9]*$") 32 | 33 | for currentIdx < len(input) { 34 | startIndex := currentIdx 35 | char := input[currentIdx] 36 | nextChar, _ := getNextCharacter(input, currentIdx) 37 | 38 | tokenType, isSymbol := tokenLookup[rune(char)] 39 | token := Token{Type: tokenType, Lexeme: string(char), Line: currentLine} 40 | 41 | if isSymbol { 42 | // Token exception cases 43 | switch tokenType { 44 | case NEWLINE: 45 | currentLine++ 46 | currentIdx++ 47 | continue 48 | case WHITESPACE: 49 | currentIdx++ 50 | continue 51 | case COMMENT: 52 | seekCharacter(input, ¤tIdx, '\n') 53 | continue 54 | } 55 | 56 | // Check for double symbol (!=, >= etc) 57 | if nextType, ok := tokenLookup[nextChar]; ok && nextType == EQUAL { 58 | jointSymbol := strings.Join([]string{string(char), string(nextChar)}, "") 59 | token.Lexeme = jointSymbol 60 | token.Type = doubleTokenLookup[jointSymbol] 61 | currentIdx++ // Skip next char 62 | } 63 | 64 | // Seek closing string 65 | if tokenType == STRING { 66 | if seekCharacter(input, ¤tIdx, '"') { 67 | return tokens, fmt.Errorf(ErrUnterminatedString.Error(), currentLine) 68 | } 69 | 70 | str := intervalToString(input, startIndex, currentIdx) 71 | token.Lexeme = str 72 | token.Literal = str[1 : len(str)-1] 73 | } 74 | 75 | tokens = append(tokens, token) 76 | currentIdx++ 77 | continue 78 | } 79 | 80 | // Not alpha numeric (a-z 0-9 _.) 81 | if !alphaNumRegex.MatchString(string(char)) { 82 | return tokens, fmt.Errorf(ErrUnexpectedToken.Error(), string(char), currentLine) 83 | } 84 | 85 | // Char is not a symbol and is the start of an identifier, keyword, or number 86 | seekFunc(input, ¤tIdx, func(c rune) bool { 87 | return !alphaNumRegex.MatchString(string(c)) 88 | }) 89 | 90 | identifier := intervalToString(input, startIndex, currentIdx) 91 | number, err := strconv.ParseFloat(identifier, 64) 92 | token.Lexeme = identifier 93 | 94 | isNumber := err == nil 95 | isAlphaNum := variableRegex.MatchString(identifier) 96 | 97 | splitDot := strings.Split(identifier, ".") 98 | isGetter := !isNumber && len(splitDot) > 1 99 | 100 | invalidSyntax := fmt.Errorf(ErrInvalidSyntax.Error(), identifier, currentLine) 101 | if !isNumber && !isAlphaNum && !isGetter { 102 | return tokens, invalidSyntax 103 | } 104 | 105 | if isGetter { 106 | dot, err := GetTokens(".") 107 | if err != nil { 108 | return tokens, err 109 | } 110 | 111 | ts := []Token{} 112 | for _, ident := range splitDot { 113 | t, err := GetTokens(ident) 114 | if err != nil { 115 | return tokens, err 116 | } 117 | 118 | if len(t) == 0 { 119 | return tokens, invalidSyntax 120 | } 121 | 122 | t[0].Line = token.Line 123 | ts = append(ts, dot...) 124 | ts = append(ts, t...) 125 | } 126 | 127 | // Shift 1 to skip first dot 128 | tokens = append(tokens, ts[1:]...) 129 | currentIdx++ 130 | continue 131 | } 132 | 133 | if isNumber { 134 | token.Literal = number 135 | token.Type = NUMBER 136 | } 137 | 138 | if isAlphaNum { 139 | token.Type = IDENTIFIER 140 | if keywordType, isKeyword := keyWordLookup[identifier]; isKeyword { 141 | // Set literal values for keyword types 142 | switch keywordType { 143 | case FALSE: 144 | token.Literal = false 145 | case TRUE: 146 | token.Literal = true 147 | case NIL: 148 | token.Literal = nil 149 | } 150 | 151 | token.Type = keywordType 152 | } 153 | } 154 | 155 | tokens = append(tokens, token) 156 | currentIdx++ 157 | } 158 | 159 | return tokens, err 160 | } 161 | 162 | // Returns the next character in the input without consuming it 163 | func getNextCharacter(input string, curIdx int) (nextChar rune, eof bool) { 164 | if curIdx < len(input)-1 { 165 | return rune(input[curIdx+1]), false 166 | } 167 | 168 | return nextChar, true 169 | } 170 | 171 | // Consumes characters until the matchFunc returns true. If eof is reached true is returned. 172 | // Modifies curIdx value. Does not consume final character. 173 | func seekFunc(input string, curIdx *int, matchFunc func(char rune) bool) (eof bool) { 174 | for { 175 | if nextChar, isEOF := getNextCharacter(input, *curIdx); !isEOF { 176 | if matchFunc(nextChar) { 177 | return false 178 | } 179 | 180 | *curIdx++ 181 | continue 182 | } 183 | 184 | return true 185 | } 186 | } 187 | 188 | // Calls seekFunc to match the target character. Consumes final character. 189 | func seekCharacter(input string, curIdx *int, target rune) (eof bool) { 190 | eof = seekFunc(input, curIdx, func(char rune) bool { 191 | return char == target 192 | }) 193 | 194 | *curIdx++ 195 | return eof 196 | } 197 | 198 | // Takes an index interval in the input and returns the string 199 | func intervalToString(input string, startIdx int, endIdx int) string { 200 | result := "" 201 | for i := startIdx; i <= endIdx; i++ { 202 | cur := input[i] 203 | 204 | // Check for special characters 205 | if cur == '\\' && len(input) > i+1 { 206 | char := "" 207 | switch input[i+1] { 208 | case 'n': 209 | char = "\n" 210 | case 't': 211 | char = "\t" 212 | } 213 | 214 | i++ 215 | result += char 216 | continue 217 | } 218 | 219 | result += string(cur) 220 | } 221 | 222 | return result 223 | } 224 | -------------------------------------------------------------------------------- /lexer/token_types.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | const ( 4 | // Ordered by precedence lo -> hi 5 | NOT_TOKEN = iota 6 | 7 | // Expression types 8 | AND 9 | OR 10 | EQUAL_EQUAL 11 | NOT_EQUAL 12 | IN 13 | GREATER 14 | LESS 15 | GREATER_EQUAL 16 | LESS_EQUAL 17 | 18 | PLUS 19 | MINUS 20 | STAR 21 | SLASH 22 | MODULO 23 | HAT 24 | 25 | TYPE 26 | NOT 27 | 28 | STRING 29 | NUMBER 30 | TRUE 31 | FALSE 32 | NIL 33 | IDENTIFIER 34 | 35 | // Not valid expression types 36 | LEFT_PAREN 37 | RIGHT_PAREN 38 | LEFT_BRACE 39 | RIGHT_BRACE 40 | LEFT_SQUARE 41 | RIGHT_SQUARE 42 | COMMA 43 | DOT 44 | SEMICOLON 45 | COMMENT 46 | EQUAL 47 | DEF_EQUAL 48 | PLUS_EQUAL 49 | MINUS_EQUAL 50 | MULT_EQUAL 51 | DIV_EQUAL 52 | 53 | // Statement tokens 54 | FUNC 55 | DEFINE 56 | ENUM 57 | IF 58 | ELSE 59 | RANGE 60 | PRINT 61 | EXIT 62 | ERROR 63 | VAR 64 | WHILE 65 | BREAK 66 | SKIP 67 | IMPORT 68 | INCLUDE 69 | REPEAT 70 | RETURN 71 | 72 | WHITESPACE 73 | NEWLINE 74 | EOF 75 | ) 76 | 77 | var tokenLookup = map[rune]int{ 78 | '\t': WHITESPACE, 79 | '\r': WHITESPACE, 80 | '\n': NEWLINE, 81 | ' ': WHITESPACE, 82 | 83 | ')': RIGHT_PAREN, 84 | '(': LEFT_PAREN, 85 | '{': LEFT_BRACE, 86 | '}': RIGHT_BRACE, 87 | '[': LEFT_SQUARE, 88 | ']': RIGHT_SQUARE, 89 | 90 | '*': STAR, 91 | '/': SLASH, 92 | '-': MINUS, 93 | '+': PLUS, 94 | '^': HAT, 95 | '%': MODULO, 96 | 97 | ';': SEMICOLON, 98 | ',': COMMA, 99 | '.': DOT, 100 | '#': COMMENT, 101 | '"': STRING, 102 | 103 | '=': EQUAL, 104 | '!': NOT, 105 | '>': GREATER, 106 | '<': LESS, 107 | '&': AND, 108 | ':': OR, 109 | } 110 | 111 | var doubleTokenLookup = map[string]int{ 112 | "==": EQUAL_EQUAL, 113 | "!=": NOT_EQUAL, 114 | ">=": GREATER_EQUAL, 115 | "<=": LESS_EQUAL, 116 | "+=": PLUS_EQUAL, 117 | "-=": MINUS_EQUAL, 118 | "*=": MULT_EQUAL, 119 | "/=": DIV_EQUAL, 120 | ":=": DEF_EQUAL, 121 | } 122 | 123 | var keyWordLookup = map[string]int{ 124 | "if": IF, 125 | "else": ELSE, 126 | "nil": NIL, 127 | "print": PRINT, 128 | "return": RETURN, 129 | "exit": EXIT, 130 | "define": DEFINE, 131 | "var": VAR, 132 | "true": TRUE, 133 | "false": FALSE, 134 | "while": WHILE, 135 | "func": FUNC, 136 | "type": TYPE, 137 | "break": BREAK, 138 | "import": IMPORT, 139 | "repeat": REPEAT, 140 | "skip": SKIP, 141 | "include": INCLUDE, 142 | "error": ERROR, 143 | "in": IN, 144 | "enum": ENUM, 145 | "range": RANGE, 146 | } 147 | -------------------------------------------------------------------------------- /lib/autodocs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from genericpath import isdir 3 | 4 | # Generates documentation for libraries from the function docstrings 5 | # and definitions. The documentation is created as a markdown document. 6 | # Also creates a function 'dump' file. It is used when running the fizz 7 | # 'docs' subommand. 8 | 9 | def main(): 10 | libraries = os.listdir("./lib") 11 | for foldername in libraries: 12 | # Check for file not directory 13 | if len(foldername.split(".")) != 1: 14 | continue 15 | 16 | # Get library files in folder 17 | path = f"./lib/{foldername}" 18 | files = os.listdir(path) 19 | 20 | total = [] 21 | for file in files: 22 | # Must be Go file 23 | if not file.endswith(".go"): 24 | continue 25 | 26 | # Read file and add formatted comments to total 27 | content = open(f"{path}/{file}").readlines() 28 | total.extend(add_doc(content)) 29 | 30 | if len(total) != 0: 31 | # Write doc content to md file 32 | create_file(path, foldername, total) 33 | 34 | 35 | # Returns formatted go comment as markdown section 36 | def add_doc(c: str): 37 | # Total comments/messages 38 | docs = [] 39 | 40 | start = 0 41 | for idx, l in enumerate(c): 42 | # Remove tabs 43 | raw = l.replace("\t", "") 44 | # One liner comment 45 | if raw.startswith("/*") and l.count("*/") == 1: 46 | # Append content bewteen pairs 47 | docs.append(("", f"{raw[3:len(raw)-3]}\n")) 48 | continue 49 | 50 | # Multiline comment, get content between pairs 51 | if l.startswith("/*"): 52 | start = idx 53 | if l.startswith("*/"): 54 | interval = c[start+1:idx] 55 | # Remove tabs 56 | doc = "".join(interval[:len(interval)-1]).replace("\t", "") 57 | func = interval[len(interval)-1].replace("\t", "") 58 | docs.append((doc, func)) 59 | 60 | return docs 61 | 62 | 63 | # Write docs to file (create file) 64 | def create_file(path: str, libname: str, docs: list[list[str]]): 65 | # Create dump file if it doesnt exist 66 | dump = "lib/_libdump" 67 | if not isdir(dump): 68 | print("[INFO] _libdump was not present, creating it now") 69 | os.mkdir(dump) 70 | 71 | filename = f"{dump}/{libname}.txt" 72 | os.open(filename, os.O_CREAT) 73 | 74 | # Write function dump 75 | with open(filename, "w+") as f: 76 | for func in docs: 77 | f.write(f"{func[1]}\t{func[0]}\n") 78 | 79 | # Create file if it doesnt exist already 80 | filename = f"{path}/{libname}_docs.md" 81 | os.open(filename, os.O_CREAT) 82 | 83 | # Open and write formatted with markdown 84 | with open(filename, "w+") as f: 85 | # Write formatted function documentation 86 | f.write(f"# Methods in {libname} library\n\n") 87 | for func in docs: 88 | name = func[1].split(" ")[1].split("(")[0] 89 | f.write(f"## **`{name}`**\n\n") 90 | f.write(f"{func[0]}\n") 91 | f.write(f"```go\n{func[1]}```\n\n") 92 | f.write("
\n\n") 93 | 94 | 95 | if __name__ == "__main__": 96 | main() -------------------------------------------------------------------------------- /lib/build.py: -------------------------------------------------------------------------------- 1 | import os, subprocess 2 | from genericpath import isdir, isfile 3 | 4 | if __name__ == "__main__": 5 | # Create init functions for libraries. This is to export the public 6 | # functions through the Include map. Creates a new 'export.go' file 7 | # (temporary) where the functions are included automatically, where 8 | # only functions with capital first letters (go standard) are exported. 9 | libs = os.listdir("lib") 10 | for lib in libs: 11 | if not isdir(f"lib/{lib}"): 12 | continue 13 | 14 | for file in os.listdir(f"lib/{lib}"): 15 | # Directory or not go file 16 | if len(file.split(".")) == 1 or file.split(".")[1] != "go": 17 | continue 18 | 19 | exports: list[str] = [] # list of function names 20 | with open(f"lib/{lib}/{file}", "r+") as f: 21 | lines = f.readlines() 22 | for line in lines: 23 | if not line.startswith("func"): 24 | continue 25 | 26 | # First char in function name 27 | is_export = line.split(" ")[1][0].isupper() 28 | if not is_export: 29 | continue 30 | 31 | func_name = line.split(" ")[1].split("(")[0] 32 | exports.append(func_name) 33 | 34 | if len(exports) == 0: 35 | continue 36 | 37 | export_file = f"lib/{lib}/export.go" 38 | if not isfile(export_file): 39 | os.open(export_file, os.O_CREAT) 40 | 41 | with open(export_file, "w+") as f: 42 | f.write("// AUTO-GENERATED FOR BUILD, DO NOT EDIT\n") 43 | f.write("// https://github.com/jesperkha/Fizz/blob/main/docs/libraries.md\n") 44 | f.write(f"package {lib}\n") 45 | f.write("var Includes = map[string]interface{}{}\n") 46 | f.write("func init() {\n") 47 | for name in exports: 48 | lower_name = name[0].lower() + name[1:] 49 | f.write(f'Includes["{lower_name}"] = {name}\n') 50 | f.write("}") 51 | 52 | # Write library imports to include.go file in /lib. Since Go plugins are 53 | # only available on linux this is the next best option. Library dependencies 54 | # are automatically added with the name of the folder. (this ofcourse means 55 | # that the package name should be the same). Otherwise an undefined variable 56 | # error will be raised. 57 | filename = "lib/include.go" 58 | if not isfile(filename): 59 | os.open(filename, os.O_CREAT) 60 | 61 | with open(filename, "w+") as f: 62 | f.write("package lib\n") 63 | f.write("func init() {\n") 64 | 65 | count = 0 66 | contents = os.listdir("lib") 67 | for dirname in contents: 68 | if not isdir(f"lib/{dirname}") or dirname == "_libdump": 69 | continue 70 | 71 | # Go will raise error at compile time if there is something wrong here 72 | if isfile(f"lib/{dirname}/export.go"): 73 | f.write(f'Add("{dirname}", {dirname}.Includes)\n') 74 | count += 1 75 | else: 76 | print(f"[WARNING] Library '{dirname}' doesnt export any functions") 77 | 78 | f.write("}") 79 | 80 | # Finally add the imports 81 | cmd_imports = "goimports -w lib" 82 | print(f"[CMD] {cmd_imports}") 83 | try: 84 | subprocess.call(cmd_imports) 85 | except: 86 | print("[FATAL] Failed to run goimports. Install:\ngo install golang.org/x/tools/cmd/goimports@latest") 87 | exit(1) 88 | 89 | print(f"[INFO] Included {count} libraries") -------------------------------------------------------------------------------- /lib/io/io.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io/fs" 8 | "io/ioutil" 9 | "os" 10 | 11 | "github.com/jesperkha/Fizz/env" 12 | ) 13 | 14 | // Standard io package for standard io operations 15 | 16 | type i interface{} 17 | 18 | var ( 19 | scanner = bufio.NewScanner(os.Stdin) 20 | ErrInvalidPath = errors.New("invalid path, line %d") 21 | ) 22 | 23 | /* 24 | Gets user input from stdin. 25 | func input(prompt string) string 26 | */ 27 | func Input(prompt string) (input i, err error) { 28 | fmt.Print(prompt) 29 | scanner.Scan() 30 | return scanner.Text(), nil 31 | } 32 | 33 | /* 34 | Reads file and returns text. 35 | func readFile(filename string) string 36 | */ 37 | func ReadFile(filename string) (str i, err error) { 38 | content, err := ioutil.ReadFile(filename) 39 | if err != nil { 40 | return str, ErrInvalidPath 41 | } 42 | 43 | return string(content), err 44 | } 45 | 46 | /* 47 | Writes content to file. Overwrites previous file content. 48 | func writeFile(filename string, content string) 49 | */ 50 | func WriteFile(filename string, content string) (val i, err error) { 51 | err = ioutil.WriteFile(filename, []byte(content), fs.ModeAppend) 52 | if err != nil { 53 | return val, ErrInvalidPath 54 | } 55 | 56 | return val, err 57 | } 58 | 59 | /* 60 | Appends content to file. 61 | func appendFile(filename string, content string) 62 | */ 63 | func AppendFile(filename string, content string) (val i, err error) { 64 | f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, fs.ModeAppend) 65 | if err != nil { 66 | return val, ErrInvalidPath 67 | } 68 | 69 | _, err = f.WriteString(content) 70 | if err != nil { 71 | return val, err 72 | } 73 | 74 | f.Close() 75 | return val, err 76 | } 77 | 78 | /* 79 | Returns list of files/directories in dir. 80 | func readDir(dir string) []string 81 | */ 82 | func ReadDir(dir string) (val i, err error) { 83 | dirs, err := os.ReadDir(dir) 84 | if err != nil { 85 | return nil, ErrInvalidPath 86 | } 87 | 88 | names := []interface{}{} 89 | for _, d := range dirs { 90 | names = append(names, d.Name()) 91 | } 92 | 93 | return env.NewArray(names), err 94 | } 95 | 96 | /* 97 | Returns current working directory 98 | func curDir() string 99 | */ 100 | func CurDir() (str i, err error) { 101 | return os.Getwd() 102 | } 103 | 104 | /* 105 | Returns true if file exists. 106 | func exists(filename string) bool 107 | */ 108 | func Exists(filename string) (val i, err error) { 109 | _, e := os.Open(filename) 110 | return e == nil, err 111 | } 112 | 113 | /* 114 | Creates new directory. 115 | func newDir(name string) 116 | */ 117 | func NewDir(dirname string) (val i, err error) { 118 | os.Mkdir(dirname, os.ModeAppend) 119 | return nil, err 120 | } 121 | 122 | /* 123 | Creates new file. If the file already exists it will be overwritten. 124 | func newFile(name string) 125 | */ 126 | func NewFile(filename string) (val i, err error) { 127 | f, err := os.Create(filename) 128 | f.Close() 129 | return val, err 130 | } 131 | -------------------------------------------------------------------------------- /lib/io/io_docs.md: -------------------------------------------------------------------------------- 1 | # Methods in io library 2 | 3 | ## **`input`** 4 | 5 | Gets user input from stdin. 6 | 7 | ```go 8 | func input(prompt string) string 9 | ``` 10 | 11 |
12 | 13 | ## **`readFile`** 14 | 15 | Reads file and returns text. 16 | 17 | ```go 18 | func readFile(filename string) string 19 | ``` 20 | 21 |
22 | 23 | ## **`writeFile`** 24 | 25 | Writes content to file. Overwrites previous file content. 26 | 27 | ```go 28 | func writeFile(filename string, content string) 29 | ``` 30 | 31 |
32 | 33 | ## **`appendFile`** 34 | 35 | Appends content to file. 36 | 37 | ```go 38 | func appendFile(filename string, content string) 39 | ``` 40 | 41 |
42 | 43 | ## **`readDir`** 44 | 45 | Returns list of files/directories in dir. 46 | 47 | ```go 48 | func readDir(dir string) []string 49 | ``` 50 | 51 |
52 | 53 | ## **`curDir`** 54 | 55 | Returns current working directory 56 | 57 | ```go 58 | func curDir() string 59 | ``` 60 | 61 |
62 | 63 | ## **`exists`** 64 | 65 | Returns true if file exists. 66 | 67 | ```go 68 | func exists(filename string) bool 69 | ``` 70 | 71 |
72 | 73 | ## **`newDir`** 74 | 75 | Creates new directory. 76 | 77 | ```go 78 | func newDir(name string) 79 | ``` 80 | 81 |
82 | 83 | ## **`newFile`** 84 | 85 | Creates new file. If the file already exists it will be overwritten. 86 | 87 | ```go 88 | func newFile(name string) 89 | ``` 90 | 91 |
92 | 93 | -------------------------------------------------------------------------------- /lib/lib.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/jesperkha/Fizz/env" 9 | "github.com/jesperkha/Fizz/util" 10 | ) 11 | 12 | type i interface{} 13 | type FuncMap map[string]interface{} 14 | 15 | var ( 16 | LibList = map[string]FuncMap{} 17 | ErrNotLib = errors.New("'%s' is not a library") 18 | ErrNotFunction = errors.New("in lib %s, '%s' is not a function") 19 | ErrNumReturn = errors.New("in lib %s, '%s()' return incorrect number of values") 20 | ErrReturnTypes = errors.New("in lib %s, '%s()' does not return (interface{}, error)") 21 | ) 22 | 23 | // Adds inclusion map to global include list if lib name is required 24 | func Add(libName string, functions FuncMap) { 25 | if _, ok := LibList[libName]; ok { 26 | util.ErrorAndExit(fmt.Errorf("duplicate package name '%s'", libName)) 27 | } 28 | 29 | LibList[libName] = functions 30 | } 31 | 32 | // Checks if function is valid. Returns error if not. 33 | func VerifyFunction(lib string, fname string, f i) error { 34 | if reflect.TypeOf(f).Kind() != reflect.Func { 35 | return fmt.Errorf(ErrNotFunction.Error(), lib, fname) 36 | } 37 | 38 | typ := reflect.TypeOf(f) 39 | if typ.NumOut() != 2 { 40 | return fmt.Errorf(ErrNumReturn.Error(), lib, fname) 41 | } 42 | 43 | if typ.Out(0).Kind() != reflect.Interface || typ.Out(1).Kind() != reflect.Interface { 44 | return fmt.Errorf(ErrReturnTypes.Error(), lib, fname) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | // Imports all included libs to running fizz process 51 | func IncludeLibraries(includes []string) error { 52 | for _, name := range includes { 53 | // Check if name is in list of valid libs 54 | library, ok := LibList[name] 55 | if !ok { 56 | return fmt.Errorf(ErrNotLib.Error(), name) 57 | } 58 | 59 | // Declare all functions in library. Push new scope, declare functions 60 | // and then get env before popping scope again. This simulates a file 61 | // import and the functions are added the same way as normal imports. 62 | env.PushScope() 63 | for funcName, f := range library { 64 | // Verify functions at runtime to not lag on startup 65 | err := VerifyFunction(name, funcName, f) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Cache values because they are used in the Call member, which will use 71 | // the closured version of the variables, and they will always be the last 72 | // used in the loop. Cache makes sure its always the ones at definition. 73 | nameCache, funcCache := funcName, f 74 | 75 | // Create function. -1 ignores number of args in parsing 76 | callable := env.Callable{ 77 | NumArgs: -1, 78 | Origin: name, 79 | Name: funcName, 80 | Call: func(args ...interface{}) (interface{}, error) { 81 | return CallFunc(nameCache, funcCache, args) 82 | }, 83 | } 84 | 85 | // Declare to scope it was required in 86 | if err := env.Declare(funcName, &callable); err != nil { 87 | return err 88 | } 89 | } 90 | 91 | // Get env and pop. Add as import 92 | defEnv := env.GetCurrentEnv() 93 | env.PopScope() 94 | if err := env.AddImportedFile(name, defEnv); err != nil { 95 | return err 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | 102 | // Calls library function from fizz process. Uses reflect to get arg number and 103 | // types from function. This means the types of params matter (but only here). 104 | // Returns error if args or types dont match. Name of function and name given do 105 | // not need to match. 106 | func CallFunc(name string, function i, args []interface{}) (val i, err error) { 107 | // Get value as type 108 | f := reflect.ValueOf(function) 109 | 110 | // Check if num args are valid 111 | numArgs := f.Type().NumIn() 112 | gotArgs := len(args) 113 | if numArgs != gotArgs { 114 | s := fmt.Sprintf("%s() expected %d args, got %d", name, numArgs, gotArgs) 115 | return val, errors.New(s + ", line %d") 116 | } 117 | 118 | // Convert args to reflect.Value 119 | argsIn := make([]reflect.Value, numArgs) 120 | for idx, value := range args { 121 | // Arg value types must match for lib functions 122 | paramType := f.Type().In(idx) 123 | argType := reflect.TypeOf(value) 124 | // Interface param type doesnt need type check. 125 | // Unsafe: i might not be defined as interface 126 | if paramType != argType && paramType.Name() != "i" { 127 | // Get fizz names for types 128 | expect := util.GetLibType(paramType.Name()) 129 | got := util.GetType(value) 130 | 131 | // Do crazy error shenanigans to avoid writing twice 132 | s := "%s() expected arg %d to be %s, got %s" 133 | e := fmt.Sprintf(s, name, idx+1, expect, got) 134 | return val, errors.New(e + ", line %d") 135 | } 136 | 137 | argsIn[idx] = reflect.ValueOf(value) 138 | } 139 | 140 | // Call function with args 141 | res := f.Call(argsIn) 142 | value := res[0].Interface() 143 | // Get error. Return interface value is techically never nil so an 144 | // explicit check is performed. 145 | if res[1].IsNil() { 146 | err = nil 147 | } else { 148 | err = res[1].Interface().(error) 149 | } 150 | 151 | // Returned value must be error based in map type 152 | return value, err 153 | } 154 | -------------------------------------------------------------------------------- /lib/math/math.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | // Standard math package for most common mathematical operations 10 | 11 | type i interface{} 12 | 13 | func init() { 14 | rand.Seed(time.Hour.Milliseconds()) 15 | } 16 | 17 | /* func sin(num float64) float64 */ 18 | func Sin(num float64) (val i, err error) { 19 | return math.Sin(num), err 20 | } 21 | 22 | /* func cos(num float64) float64 */ 23 | func Cos(num float64) (val i, err error) { 24 | return math.Cos(num), err 25 | } 26 | 27 | /* func asin(num float64) float64 */ 28 | func Asin(num float64) (val i, err error) { 29 | return math.Asin(num), err 30 | } 31 | 32 | /* func acos(num float64) float64 */ 33 | func Acos(num float64) (val i, err error) { 34 | return math.Acos(num), err 35 | } 36 | 37 | /* func tan(num float64) float64 */ 38 | func Tan(num float64) (val i, err error) { 39 | return math.Tan(num), err 40 | } 41 | 42 | /* func atan(num float64) float64 */ 43 | func Atan(num float64) (val i, err error) { 44 | return math.Atan(num), err 45 | } 46 | 47 | /* func floor(num float64) float64 */ 48 | func Floor(num float64) (val i, err error) { 49 | return math.Floor(num), err 50 | } 51 | 52 | /* func ceil(num float64) float64 */ 53 | func Ceil(num float64) (val i, err error) { 54 | return math.Ceil(num), err 55 | } 56 | 57 | /* func abs(num float64) float64 */ 58 | func Abs(num float64) (val i, err error) { 59 | return math.Abs(num), err 60 | } 61 | 62 | /* func ln(num float64) float64 */ 63 | func Ln(num float64) (val i, err error) { 64 | return math.Log(num), err 65 | } 66 | 67 | /* func log10(num float64) float64 */ 68 | func Log10(num float64) (val i, err error) { 69 | return math.Log10(num), err 70 | } 71 | 72 | /* func sqrt(num float64) float64 */ 73 | func Sqrt(num float64) (val i, err error) { 74 | return math.Sqrt(num), err 75 | } 76 | 77 | /* func max(a float64, b float64) float64 */ 78 | func Max(a float64, b float64) (val i, err error) { 79 | return math.Max(a, b), err 80 | } 81 | 82 | /* func min(a float64, b float64) float64 */ 83 | func Min(a float64, b float64) (val i, err error) { 84 | return math.Min(a, b), err 85 | } 86 | 87 | /* 88 | Converts degrees to radians 89 | func rad(deg float64) float64 90 | */ 91 | func Rad(num float64) (val i, err error) { 92 | return (math.Pi * 2 * num) / 360, err 93 | } 94 | 95 | /* 96 | Converts radians to degrees 97 | func deg(rad float64) float64 98 | */ 99 | func Deg(num float64) (val i, err error) { 100 | return (num * 360) / (2 * math.Pi), err 101 | } 102 | 103 | /* 104 | Gets random number bewteen 0 and 1 105 | func random() float64 106 | */ 107 | func Random() (val i, err error) { 108 | return rand.Float64(), err 109 | } 110 | -------------------------------------------------------------------------------- /lib/math/math_docs.md: -------------------------------------------------------------------------------- 1 | # Methods in math library 2 | 3 | ## **`sin`** 4 | 5 | 6 | ```go 7 | func sin(num float64) float64 8 | ``` 9 | 10 |
11 | 12 | ## **`cos`** 13 | 14 | 15 | ```go 16 | func cos(num float64) float64 17 | ``` 18 | 19 |
20 | 21 | ## **`asin`** 22 | 23 | 24 | ```go 25 | func asin(num float64) float64 26 | ``` 27 | 28 |
29 | 30 | ## **`acos`** 31 | 32 | 33 | ```go 34 | func acos(num float64) float64 35 | ``` 36 | 37 |
38 | 39 | ## **`tan`** 40 | 41 | 42 | ```go 43 | func tan(num float64) float64 44 | ``` 45 | 46 |
47 | 48 | ## **`atan`** 49 | 50 | 51 | ```go 52 | func atan(num float64) float64 53 | ``` 54 | 55 |
56 | 57 | ## **`floor`** 58 | 59 | 60 | ```go 61 | func floor(num float64) float64 62 | ``` 63 | 64 |
65 | 66 | ## **`ceil`** 67 | 68 | 69 | ```go 70 | func ceil(num float64) float64 71 | ``` 72 | 73 |
74 | 75 | ## **`abs`** 76 | 77 | 78 | ```go 79 | func abs(num float64) float64 80 | ``` 81 | 82 |
83 | 84 | ## **`ln`** 85 | 86 | 87 | ```go 88 | func ln(num float64) float64 89 | ``` 90 | 91 |
92 | 93 | ## **`log10`** 94 | 95 | 96 | ```go 97 | func log10(num float64) float64 98 | ``` 99 | 100 |
101 | 102 | ## **`sqrt`** 103 | 104 | 105 | ```go 106 | func sqrt(num float64) float64 107 | ``` 108 | 109 |
110 | 111 | ## **`max`** 112 | 113 | 114 | ```go 115 | func max(a float64, b float64) float64 116 | ``` 117 | 118 |
119 | 120 | ## **`min`** 121 | 122 | 123 | ```go 124 | func min(a float64, b float64) float64 125 | ``` 126 | 127 |
128 | 129 | ## **`rad`** 130 | 131 | Converts degrees to radians 132 | 133 | ```go 134 | func rad(deg float64) float64 135 | ``` 136 | 137 |
138 | 139 | ## **`deg`** 140 | 141 | Converts radians to degrees 142 | 143 | ```go 144 | func deg(rad float64) float64 145 | ``` 146 | 147 |
148 | 149 | ## **`random`** 150 | 151 | Gets random number bewteen 0 and 1 152 | 153 | ```go 154 | func random() float64 155 | ``` 156 | 157 |
158 | 159 | -------------------------------------------------------------------------------- /lib/print.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | var ( 10 | ErrNotALibrary = errors.New("'%s' is not a known library") 11 | 12 | //go:embed _libdump 13 | embeddedDocs embed.FS 14 | ) 15 | 16 | // Prints functions of the given library to the terminal. Returns 17 | // an error if not a known library name. 18 | func PrintDocs(libname string) error { 19 | filename := fmt.Sprintf("_libdump/%s.txt", libname) 20 | file, err := embeddedDocs.ReadFile(filename) 21 | if err != nil { 22 | return fmt.Errorf(ErrNotALibrary.Error(), libname) 23 | } 24 | 25 | fmt.Println() 26 | fmt.Print(string(file)) 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /lib/str/str.go: -------------------------------------------------------------------------------- 1 | package str 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/jesperkha/Fizz/env" 10 | "github.com/jesperkha/Fizz/util" 11 | ) 12 | 13 | // Standard string package for common string fuctionality 14 | 15 | type i interface{} 16 | 17 | var ( 18 | ErrNotNumber = errors.New("string could not be converted to number, line %d") 19 | ErrNotString = errors.New("expected string value in array, line %d") 20 | ) 21 | 22 | /* 23 | Converts value to string. 24 | func toString(value interface{}) string 25 | */ 26 | func ToString(val i) (str i, err error) { 27 | return fmt.Sprint(val), err 28 | } 29 | 30 | /* 31 | Formats value to default Fizz print formatting. 32 | func format(value interface{}) string 33 | */ 34 | func Format(val i) (str i, err error) { 35 | return util.FormatPrintValue(val), err 36 | } 37 | 38 | /* 39 | Converts all letters in string to lower case. 40 | func lower(str string) string 41 | */ 42 | func Lower(str string) (val i, err error) { 43 | return strings.ToLower(str), err 44 | } 45 | 46 | /* 47 | Converts all letters in string to upper case. 48 | func upper(str string) string 49 | */ 50 | func Upper(str string) (val i, err error) { 51 | return strings.ToUpper(str), err 52 | } 53 | 54 | /* 55 | Capitalizes the letter at the beginning of each word. 56 | func capital(str string) string 57 | */ 58 | func Capital(str string) (val i, err error) { 59 | return strings.Title(str), err 60 | } 61 | 62 | /* 63 | Splits string by substring 64 | func split(str string, split string) []string 65 | */ 66 | func Split(str string, split string) (val i, err error) { 67 | splits := []interface{}{} 68 | for _, s := range strings.Split(str, split) { 69 | splits = append(splits, s) 70 | } 71 | 72 | return env.NewArray(splits), err 73 | } 74 | 75 | /* 76 | Joins array of strings into one string with the substring. 77 | func join(strings []string, sub string) string 78 | */ 79 | func Join(str *env.Array, sub string) (val i, err error) { 80 | s := []string{} 81 | for _, i := range str.Values { 82 | if newStr, ok := i.(string); ok { 83 | s = append(s, newStr) 84 | continue 85 | } 86 | 87 | return val, ErrNotString 88 | } 89 | 90 | return strings.Join(s, sub), err 91 | } 92 | 93 | /* 94 | Replaces all instances of substring with new string. 95 | func replace(str string, old string, new string) string 96 | */ 97 | func Replace(str string, old string, new string) (val i, err error) { 98 | return strings.ReplaceAll(str, old, new), err 99 | } 100 | 101 | /* 102 | Converts string to number. 103 | func toNumber(str string) float64 104 | */ 105 | func ToNumber(val string) (num i, err error) { 106 | num, err = strconv.ParseFloat(val, 64) 107 | if err != nil { 108 | return num, ErrNotNumber 109 | } 110 | 111 | return num, err 112 | } 113 | -------------------------------------------------------------------------------- /lib/str/str_docs.md: -------------------------------------------------------------------------------- 1 | # Methods in str library 2 | 3 | ## **`toString`** 4 | 5 | Converts value to string. 6 | 7 | ```go 8 | func toString(value interface{}) string 9 | ``` 10 | 11 |
12 | 13 | ## **`format`** 14 | 15 | Formats value to default Fizz print formatting. 16 | 17 | ```go 18 | func format(value interface{}) string 19 | ``` 20 | 21 |
22 | 23 | ## **`lower`** 24 | 25 | Converts all letters in string to lower case. 26 | 27 | ```go 28 | func lower(str string) string 29 | ``` 30 | 31 |
32 | 33 | ## **`upper`** 34 | 35 | Converts all letters in string to upper case. 36 | 37 | ```go 38 | func upper(str string) string 39 | ``` 40 | 41 |
42 | 43 | ## **`capital`** 44 | 45 | Capitalizes the letter at the beginning of each word. 46 | 47 | ```go 48 | func capital(str string) string 49 | ``` 50 | 51 |
52 | 53 | ## **`split`** 54 | 55 | Splits string by substring 56 | 57 | ```go 58 | func split(str string, split string) []string 59 | ``` 60 | 61 |
62 | 63 | ## **`join`** 64 | 65 | Joins array of strings into one string with the substring. 66 | 67 | ```go 68 | func join(strings []string, sub string) string 69 | ``` 70 | 71 |
72 | 73 | ## **`replace`** 74 | 75 | Replaces all instances of substring with new string. 76 | 77 | ```go 78 | func replace(str string, old string, new string) string 79 | ``` 80 | 81 |
82 | 83 | ## **`toNumber`** 84 | 85 | Converts string to number. 86 | 87 | ```go 88 | func toNumber(str string) float64 89 | ``` 90 | 91 |
92 | 93 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | // Non-release version use 'dev' suffix 5 | VERSION = "1.2.0" 6 | ) 7 | 8 | func main() { 9 | RunInterpreter() 10 | } 11 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/jesperkha/Fizz/env" 11 | "github.com/jesperkha/Fizz/interp" 12 | "github.com/jesperkha/Fizz/lib" 13 | "github.com/jesperkha/Fizz/stmt" 14 | "github.com/jesperkha/Fizz/term" 15 | "github.com/jesperkha/Fizz/util" 16 | ) 17 | 18 | var ( 19 | ErrOneArgOnly = errors.New("expected a single argument, got %d") 20 | validArgs = []string{"--help", "--version", "-f", "-e"} 21 | ) 22 | 23 | func RunInterpreter() { 24 | parser, err := term.Parse(validArgs) 25 | if err != nil { 26 | util.ErrorAndExit(err) 27 | } 28 | args := parser.Args() 29 | if len(args) > 1 { 30 | util.ErrorAndExit(fmt.Errorf(ErrOneArgOnly.Error(), len(args))) 31 | } 32 | 33 | // Early exit options 34 | if parser.HasOption("help") { 35 | fmt.Println(term.HELP) 36 | return 37 | } else if parser.HasOption("version") { 38 | fmt.Printf("Fizz %s\n", VERSION) 39 | return 40 | } 41 | 42 | // Subcommands 43 | switch parser.SubCommand() { 44 | case "docs": 45 | if err := lib.PrintDocs(args[0]); err != nil { 46 | util.ErrorAndExit(err) 47 | } 48 | return 49 | case "help": 50 | if msg, ok := term.CommandDescriptions[args[0]]; ok { 51 | fmt.Println(msg) 52 | } else { 53 | util.PrintError(fmt.Errorf(term.ErrUnknownCommand.Error(), args[0])) 54 | } 55 | return 56 | } 57 | 58 | // Run terminal mode if no other args are given 59 | if len(args) == 0 { 60 | RunTerminal() 61 | return 62 | } 63 | 64 | // Goto directory of file specified 65 | split := strings.Split(args[0], "/") 66 | path := strings.Join(split[:len(split)-1], "/") 67 | name := split[len(split)-1] 68 | os.Chdir(path) 69 | 70 | // Run file 71 | e, err := interp.RunFile(name) 72 | 73 | // Print global environment if flag is set first 74 | if parser.HasFlag("e") { 75 | fmt.Println(util.FormatPrintValue(e)) 76 | } 77 | 78 | // Handle error 79 | if err != nil && err != stmt.ErrProgramExit { 80 | util.PrintError(err) 81 | if c := env.GetCallstack(); parser.HasFlag("f") && len(c) > 0 { 82 | util.PrintError(fmt.Errorf(c)) 83 | } 84 | 85 | os.Exit(1) 86 | } 87 | } 88 | 89 | // Leaves the interpreter running as the user inputs code to the terminal. 90 | // Prints out errors but does not terminate until ^C or 'exit'. 91 | func RunTerminal() { 92 | fmt.Println("type 'exit' to terminate session") 93 | scanner := bufio.NewScanner(os.Stdin) 94 | numBlocks, line := 0, 1 95 | totalString, space := "", " " 96 | env.ThrowEnvironment = false 97 | 98 | for { 99 | fmt.Printf("%d%s : %s", line, space, strings.Repeat(" ", numBlocks)) 100 | scanner.Scan() 101 | input := scanner.Text() 102 | 103 | if input == "exit" { 104 | break 105 | } 106 | 107 | // Continue with indent after braces 108 | numBlocks += strings.Count(input, "{") - strings.Count(input, "}") 109 | totalString += input + "\n" // Better error handling 110 | if numBlocks <= 0 { 111 | if _, err := interp.Interperate("", totalString); err != nil { 112 | util.PrintError(err) 113 | line-- 114 | } 115 | 116 | totalString = "" 117 | numBlocks = 0 118 | } 119 | 120 | line++ 121 | if line == 10 { 122 | space = "" 123 | } 124 | } 125 | 126 | fmt.Println("session ended") 127 | } 128 | -------------------------------------------------------------------------------- /stmt/execute.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/jesperkha/Fizz/env" 8 | "github.com/jesperkha/Fizz/expr" 9 | "github.com/jesperkha/Fizz/lexer" 10 | "github.com/jesperkha/Fizz/util" 11 | ) 12 | 13 | var ( 14 | CurrentOrigin string 15 | MaxRecursionDepth = 1000 16 | ) 17 | 18 | // Goes through list of statements and executes them. Error is returned from statements exec method. 19 | func ExecuteStatements(stmts []Statement) (err error) { 20 | for _, statement := range stmts { 21 | line := statement.Line 22 | if err = executeStatement(statement); err != nil { 23 | if cerr, ok := err.(ConditionalError); ok { 24 | cerr.Msg = fmt.Sprintf(cerr.Msg, line) 25 | return cerr 26 | } 27 | 28 | return util.FormatError(err, line) 29 | } 30 | } 31 | 32 | return err 33 | } 34 | 35 | func executeStatement(stmt Statement) error { 36 | switch stmt.Type { 37 | case ExpressionStmt: 38 | _, err := expr.EvaluateExpression(stmt.Expression) 39 | return err 40 | case Block: 41 | return execBlock(stmt) 42 | case Print: 43 | return execPrint(stmt) 44 | case Variable: 45 | return nil 46 | case Assignment: 47 | return execAssignment(stmt) 48 | case Break: 49 | return ErrBeakOutsideLoop 50 | case Skip: 51 | return ErrSkipOutsideLoop 52 | case Return: 53 | return execReturn(stmt) 54 | case If: 55 | return execIf(stmt) 56 | case While: 57 | return execWhile(stmt) 58 | case Repeat: 59 | return execRepeat(stmt) 60 | case Function: 61 | return execFunction(stmt) 62 | case Exit: 63 | return execExit(stmt) 64 | case Error: 65 | return execError(stmt) 66 | case Object: 67 | return execObject(stmt) 68 | case Enum: 69 | return execEnum(stmt) 70 | case Range: 71 | return execRange(stmt) 72 | case Import, Include: 73 | return nil // Handled in interp 74 | } 75 | 76 | // Will never be returned since all types are pre-defined. 77 | // However it is nice to have in case rework is done and types 78 | // get mixed up or new types are only partially added. 79 | return ErrInvalidStmtType 80 | } 81 | 82 | func execEnum(stmt Statement) (err error) { 83 | for curVal, name := range stmt.Params { 84 | err = env.Declare(name, float64(curVal)) 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | 90 | return err 91 | } 92 | 93 | func execExit(stmt Statement) (err error) { 94 | if stmt.Expression != nil { 95 | if err = execPrint(stmt); err != nil { 96 | return err 97 | } 98 | } 99 | 100 | return ErrProgramExit 101 | } 102 | 103 | func execError(stmt Statement) (err error) { 104 | value, err := expr.EvaluateExpression(stmt.Expression) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | return errors.New(util.FormatPrintValue(value)) 110 | } 111 | 112 | // Raises error and assigns expr value to global currentReturnValue 113 | func execReturn(stmt Statement) (err error) { 114 | e := ErrReturnOutsideFunc 115 | if stmt.Expression == nil { 116 | e.Value = nil 117 | return e 118 | } 119 | 120 | value, err := expr.EvaluateExpression(stmt.Expression) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | e.Value = value 126 | return e 127 | } 128 | 129 | func execPrint(stmt Statement) (err error) { 130 | value, err := expr.EvaluateExpression(stmt.Expression) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | fmt.Println(util.FormatPrintValue(value)) 136 | return nil 137 | } 138 | 139 | func assignValue(left *expr.Expression, value interface{}) error { 140 | if left.Type == expr.Variable { 141 | return env.Assign(left.Name, value) 142 | } 143 | 144 | // First evaluate the entire expression to pluck out any 145 | // errors that are harder to check for later 146 | if _, err := expr.EvaluateExpression(left); err != nil { 147 | return err 148 | } 149 | 150 | // Get left expression of left expression (parent) 151 | val, err := expr.EvaluateExpression(left.Left) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | // If object assign to name of parent expression 157 | if obj, ok := val.(*env.Object); ok { 158 | return obj.Set(left.Right.Name, value) 159 | } 160 | 161 | // If array assign value to index of parent expression 162 | if arr, ok := val.(*env.Array); ok { 163 | index, err := expr.EvaluateExpression(left.Right) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | indexInt, ok := util.IsInt(index) 169 | if !ok { 170 | return expr.ErrNotInteger 171 | } 172 | 173 | return arr.Set(indexInt, value) 174 | } 175 | 176 | return ErrNonAssignable 177 | } 178 | 179 | func execAssignment(stmt Statement) (err error) { 180 | val, err := expr.EvaluateExpression(stmt.Expression) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | // Plain assignment 186 | if stmt.Operator == lexer.EQUAL { 187 | return assignValue(stmt.Left, val) 188 | } 189 | 190 | // Declare variable with special := operator 191 | if stmt.Operator == lexer.DEF_EQUAL { 192 | if stmt.Left == nil || stmt.Expression == nil { 193 | return ErrInvalidStatement 194 | } 195 | 196 | return env.Declare(stmt.Left.Name, val) 197 | } 198 | 199 | oldVal, err := expr.EvaluateExpression(stmt.Left) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | // Not same type 205 | oldType, newType := util.GetType(oldVal), util.GetType(val) 206 | if oldType != newType { 207 | return ErrDifferentTypes 208 | } 209 | 210 | // String addition 211 | if newType == "string" { 212 | if stmt.Operator != lexer.PLUS_EQUAL { 213 | return ErrInvalidOperator 214 | } 215 | 216 | return assignValue(stmt.Left, oldVal.(string)+val.(string)) 217 | } 218 | 219 | // Float addition / subtraction 220 | if newType == "number" { 221 | a := oldVal.(float64) 222 | b := val.(float64) 223 | var newVal float64 224 | 225 | switch stmt.Operator { 226 | case lexer.PLUS_EQUAL: 227 | newVal = a + b 228 | case lexer.MINUS_EQUAL: 229 | newVal = a - b 230 | case lexer.MULT_EQUAL: 231 | newVal = a * b 232 | case lexer.DIV_EQUAL: 233 | newVal = a / b 234 | } 235 | 236 | return assignValue(stmt.Left, newVal) 237 | } 238 | 239 | return ErrInvalidStatement 240 | } 241 | 242 | func execBlock(stmt Statement) (err error) { 243 | env.PushScope() 244 | err = ExecuteStatements(stmt.Statements) 245 | env.PopScope() 246 | return err 247 | } 248 | 249 | // Monitor recursion 250 | var lastFunction = "" 251 | var recursionDepth = 0 252 | 253 | func execFunction(stmt Statement) (err error) { 254 | // Store origin at point of function declaration as well as scope around it 255 | originCache := CurrentOrigin 256 | var envCache env.Environment 257 | 258 | function := env.Callable{ 259 | Name: stmt.Name, 260 | NumArgs: len(stmt.Params), 261 | Origin: CurrentOrigin, 262 | // Call function and set param variables to scope 263 | Call: func(args ...interface{}) (interface{}, error) { 264 | // Handle recursion errors 265 | name := stmt.Name 266 | if lastFunction == name { 267 | recursionDepth++ 268 | } else { 269 | lastFunction = name 270 | } 271 | 272 | // Todo: better recursive checks for recursive limit 273 | if recursionDepth > MaxRecursionDepth { 274 | return nil, util.WrapFilename(originCache, ErrMaximumRecursion) 275 | } 276 | 277 | // Push closure scope into stack 278 | env.PushTempEnv(envCache) 279 | env.PushScope() 280 | 281 | // Declare args 282 | for idx, arg := range args { 283 | // Cannot raise error because block is in own scope 284 | env.Declare(stmt.Params[idx], arg) 285 | } 286 | 287 | err = ExecuteStatements(stmt.Then.Statements) 288 | env.PopScope() 289 | env.PopTempEnv() 290 | if e, ok := err.(ConditionalError); ok { 291 | return e.Value, nil 292 | } 293 | 294 | // Add to callstack 295 | if err != nil { 296 | env.FailCall(stmt.Name, originCache, stmt.Line) 297 | } 298 | 299 | return nil, util.WrapFilename(originCache, err) 300 | }, 301 | } 302 | 303 | err = env.Declare(stmt.Name, &function) 304 | // Set after function is declared to allow using the function inside its body 305 | envCache = env.GetCurrentEnv() 306 | return err 307 | } 308 | 309 | func execIf(stmt Statement) (err error) { 310 | val, err := expr.EvaluateExpression(stmt.Expression) 311 | if err != nil { 312 | return err 313 | } 314 | 315 | if val != nil && val != false { 316 | return ExecuteStatements(stmt.Then.Statements) 317 | } else if stmt.Else != nil { 318 | return ExecuteStatements(stmt.Else.Statements) 319 | } 320 | 321 | return err 322 | } 323 | 324 | func loopStatements(stmts []Statement) (brk bool, err error) { 325 | env.PushScope() 326 | err = ExecuteStatements(stmts) 327 | env.PopScope() 328 | if e, ok := err.(ConditionalError); ok { 329 | switch e.Type { 330 | case BREAK: 331 | return true, nil 332 | case SKIP: 333 | return false, nil 334 | } 335 | } 336 | 337 | return false, err 338 | } 339 | 340 | // Runs block if expression is nil too 341 | func execWhile(stmt Statement) (err error) { 342 | for { 343 | if stmt.Expression != nil { 344 | val, err := expr.EvaluateExpression(stmt.Expression) 345 | if err != nil { 346 | return err 347 | } 348 | 349 | // Only falsy values for expression 350 | if val == nil || val == false { 351 | break 352 | } 353 | } 354 | 355 | brk, err := loopStatements(stmt.Then.Statements) 356 | if err != nil { 357 | return err 358 | } 359 | 360 | if brk { 361 | break 362 | } 363 | } 364 | 365 | return err 366 | } 367 | 368 | func execRepeat(stmt Statement) (err error) { 369 | v, err := expr.EvaluateExpression(stmt.Expression) 370 | if err != nil { 371 | return err 372 | } 373 | 374 | r, ok := util.IsInt(v) 375 | if !ok { 376 | return ErrExpectedInteger 377 | } 378 | 379 | // Loop n times 380 | for i := 0; i < r; i++ { 381 | brk, err := loopStatements(stmt.Then.Statements) 382 | if err != nil { 383 | return err 384 | } 385 | 386 | if brk { 387 | break 388 | } 389 | } 390 | 391 | return err 392 | } 393 | 394 | func execObject(stmt Statement) (err error) { 395 | err = env.Declare(stmt.Name, &env.Callable{ 396 | NumArgs: len(stmt.Params), 397 | Call: func(args ...interface{}) (interface{}, error) { 398 | obj := env.Object{Fields: map[string]interface{}{}, Name: stmt.Name} 399 | for i, field := range stmt.Params { 400 | obj.Fields[field] = args[i] 401 | } 402 | 403 | return &obj, err 404 | }, 405 | }) 406 | 407 | return err 408 | } 409 | 410 | func getRangeable(args ...expr.Expression) (v *env.Array, err error) { 411 | if len(args) > 3 { 412 | return v, ErrInvalidStatement 413 | } 414 | 415 | // If array just return the array as the list of values to loop over 416 | if len(args) == 1 { 417 | val, err := expr.EvaluateExpression(&args[0]) 418 | if err != nil { 419 | return v, err 420 | } 421 | 422 | if arr, ok := val.(*env.Array); ok { 423 | return arr, err 424 | } 425 | } 426 | 427 | // Else create array of numbers in range 428 | a := []float64{} 429 | for _, e := range args { 430 | val, err := expr.EvaluateExpression(&e) 431 | if err != nil { 432 | return v, err 433 | } 434 | 435 | if num, ok := val.(float64); ok { 436 | a = append(a, num) 437 | continue 438 | } 439 | 440 | return v, ErrExpectedNumber 441 | } 442 | 443 | // Set each parameter based on how many there are 444 | nums := [3]float64{} 445 | switch len(a) { 446 | case 1: 447 | nums[0] = 0 448 | nums[1] = a[0] 449 | nums[2] = 1 450 | case 2: 451 | nums[0] = a[0] 452 | nums[1] = a[1] 453 | nums[2] = 1 454 | case 3: 455 | copy(nums[:], a) 456 | } 457 | 458 | // Check for infinite loop 459 | zero := nums[2] == 0 460 | negative := nums[1] > 0 && nums[2] <= 0 461 | if zero || negative { 462 | return v, ErrInfiniteLoop 463 | } 464 | 465 | // Create array 466 | arr := env.Array{Values: []interface{}{}} 467 | for i := nums[0]; i < nums[1]; i += nums[2] { 468 | arr.Values = append(arr.Values, i) 469 | } 470 | 471 | return &arr, err 472 | } 473 | 474 | func execRange(stmt Statement) (err error) { 475 | var rangeable *env.Array 476 | e := stmt.Expression 477 | name := stmt.Name 478 | 479 | // Set rangeable 480 | if e.Type == expr.Args { 481 | rangeable, err = getRangeable(e.Exprs...) 482 | } else { 483 | rangeable, err = getRangeable(*e) 484 | } 485 | 486 | if err != nil { 487 | return err 488 | } 489 | 490 | env.PushScope() 491 | env.Declare(name, 0.0) 492 | for _, val := range rangeable.Values { 493 | env.Assign(name, val) 494 | brk, err := loopStatements(stmt.Then.Statements) 495 | if err != nil { 496 | return err 497 | } 498 | 499 | if brk { 500 | break 501 | } 502 | } 503 | 504 | env.PopScope() 505 | return err 506 | } 507 | -------------------------------------------------------------------------------- /stmt/parse.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/jesperkha/Fizz/expr" 7 | "github.com/jesperkha/Fizz/lexer" 8 | "github.com/jesperkha/Fizz/util" 9 | ) 10 | 11 | // Parses lexer tokens into list of statements 12 | func ParseStatements(tokens []lexer.Token) (statements []Statement, err error) { 13 | currentIdx := 0 14 | 15 | for currentIdx < len(tokens) { 16 | startIndex := currentIdx 17 | firstToken := tokens[currentIdx] 18 | 19 | var currentStmt Statement 20 | line := firstToken.Line 21 | 22 | // Check conditional statements seperatly because the parse funcs need 23 | // a currentIndex pointer. Note: Full list of tokens is given 24 | currentStmt, err = parseComplexStatement(firstToken.Type, tokens, ¤tIdx) 25 | if err != nil { 26 | return statements, util.FormatError(err, line) 27 | } 28 | 29 | // Parse any other type of statement. 30 | if currentStmt.Type == NotStatement { 31 | // Seeks a semicolon since all other statements end with a semicolon 32 | endIdx, eof := seekToken(tokens, startIndex, lexer.SEMICOLON) 33 | if eof { 34 | return statements, util.FormatError(ErrNoSemicolon, line) 35 | } 36 | 37 | currentIdx = endIdx // Skip to end of statement to section off token list 38 | 39 | // Get tokens in interval between last semicolon and current one 40 | tokenInterval := tokens[startIndex:currentIdx] 41 | if len(tokenInterval) == 0 { 42 | return statements, util.FormatError(ErrNoStatement, line) 43 | } 44 | 45 | // Parse statement 46 | currentStmt, err = parseStatement(firstToken.Type, tokenInterval) 47 | if err != nil { 48 | return statements, util.FormatError(err, line) 49 | } 50 | } 51 | 52 | currentStmt.Line = line 53 | statements = append(statements, currentStmt) 54 | currentIdx++ 55 | } 56 | 57 | return statements, err 58 | } 59 | 60 | // Defaults to ExpressionStatement 61 | func parseStatement(typ int, tokens []lexer.Token) (stmt Statement, err error) { 62 | switch typ { 63 | case lexer.IDENTIFIER: 64 | return parseAssignment(tokens) 65 | case lexer.PRINT: 66 | return parsePrint(tokens) 67 | case lexer.ELSE: 68 | return parseElse(tokens) 69 | case lexer.BREAK: 70 | return parseBreak(tokens) 71 | case lexer.SKIP: 72 | return parseSkip(tokens) 73 | case lexer.RETURN: 74 | return parseReturn(tokens) 75 | case lexer.EXIT: 76 | return parseExit(tokens) 77 | case lexer.ERROR: 78 | return parseError(tokens) 79 | case lexer.IMPORT: 80 | return parseImport(tokens) 81 | case lexer.INCLUDE: 82 | return parseInclude(tokens) 83 | } 84 | 85 | return parseExpression(tokens) 86 | } 87 | 88 | // Parses statements where current index would be modified or the length of the statement is unknown 89 | func parseComplexStatement(typ int, tokens []lexer.Token, idx *int) (stmt Statement, err error) { 90 | switch typ { 91 | case lexer.IF: 92 | return parseIf(tokens, idx) 93 | case lexer.WHILE: 94 | return parseWhile(tokens, idx) 95 | case lexer.LEFT_BRACE: 96 | return parseBlock(tokens, idx) 97 | case lexer.REPEAT: 98 | return parseRepeat(tokens, idx) 99 | case lexer.FUNC: 100 | return parseFunc(tokens, idx) 101 | case lexer.DEFINE: 102 | return parseObject(tokens, idx) 103 | case lexer.ENUM: 104 | return parseEnum(tokens, idx) 105 | case lexer.RANGE: 106 | return parseRange(tokens, idx) 107 | } 108 | 109 | return stmt, err 110 | } 111 | 112 | // Returns index of target 113 | func seekToken(tokens []lexer.Token, start int, target int) (endIdx int, eof bool) { 114 | for i := start; i < len(tokens); i++ { 115 | // Missing semicolon if there are multiple statement identifiers 116 | if i > start && tokens[i].Type >= lexer.FUNC { 117 | return 0, true 118 | } 119 | 120 | if tokens[i].Type == target { 121 | return i, false 122 | } 123 | } 124 | 125 | return 0, true 126 | } 127 | 128 | func parseRange(tokens []lexer.Token, idx *int) (stmt Statement, err error) { 129 | i := *idx 130 | if len(tokens[i:]) < 6 { 131 | return stmt, ErrInvalidStatement 132 | } 133 | 134 | // Dont care about expression because of manual parsing 135 | _, block, err := getExpressionAndBlock(tokens, idx, true) 136 | if err != nil { 137 | return stmt, err 138 | } 139 | 140 | // Get left and right side of 'in' 141 | endIdx, _ := seekToken(tokens, i, lexer.LEFT_BRACE) 142 | interval := tokens[i+1 : endIdx] 143 | split := util.SplitByToken(interval, lexer.IN) 144 | if len(split) != 2 { 145 | return stmt, ErrInvalidStatement 146 | } 147 | 148 | left, err := expr.ParseExpression(split[0]) 149 | if err != nil { 150 | return stmt, err 151 | } 152 | 153 | // Only variables are identifiers 154 | if left.Type != expr.Variable { 155 | return stmt, ErrExpectedIdentifier 156 | } 157 | 158 | // Right side is just expression passed into private function 159 | right, err := expr.ParseExpression(split[1]) 160 | return Statement{Type: Range, Expression: &right, Name: left.Name, Then: &block}, err 161 | } 162 | 163 | func parseEnum(tokens []lexer.Token, idx *int) (stmt Statement, err error) { 164 | i := *idx 165 | if len(tokens[i:]) < 3 { 166 | return stmt, ErrInvalidStatement 167 | } 168 | 169 | if tokens[i+1].Type != lexer.LEFT_BRACE { 170 | return stmt, ErrExpectedBlock 171 | } 172 | 173 | endIdx, eof := seekToken(tokens, i, lexer.RIGHT_BRACE) 174 | if eof { 175 | return stmt, ErrNoBrace 176 | } 177 | 178 | names := []string{} 179 | for _, t := range tokens[i+2 : endIdx] { 180 | if t.Type != lexer.IDENTIFIER { 181 | return stmt, ErrExpectedIdentifier 182 | } 183 | 184 | names = append(names, t.Lexeme) 185 | } 186 | 187 | *idx = endIdx 188 | return Statement{Type: Enum, Params: names}, err 189 | } 190 | 191 | func parseImport(tokens []lexer.Token) (stmt Statement, err error) { 192 | if len(tokens) != 2 { 193 | return stmt, ErrInvalidStatement 194 | } 195 | 196 | if str, ok := tokens[1].Literal.(string); ok { 197 | name := strings.TrimSuffix(str, ".fizz") 198 | return Statement{Type: Import, Name: name}, err 199 | } 200 | 201 | return stmt, ErrExpectedName 202 | } 203 | 204 | func parseInclude(tokens []lexer.Token) (stmt Statement, err error) { 205 | if len(tokens) != 2 { 206 | return stmt, ErrInvalidStatement 207 | } 208 | 209 | if name, ok := tokens[1].Literal.(string); ok { 210 | return Statement{Type: Include, Name: name}, err 211 | } 212 | 213 | return stmt, ErrExpectedName 214 | } 215 | 216 | func parseExit(tokens []lexer.Token) (stmt Statement, err error) { 217 | if len(tokens) == 1 { 218 | return Statement{Type: Exit}, err 219 | } 220 | 221 | expr, err := expr.ParseExpression(tokens[1:]) 222 | return Statement{Type: Exit, Expression: &expr}, err 223 | } 224 | 225 | func parseReturn(tokens []lexer.Token) (stmt Statement, err error) { 226 | if len(tokens) == 1 { 227 | return Statement{Type: Return}, err 228 | } 229 | 230 | expr, err := expr.ParseExpression(tokens[1:]) 231 | return Statement{Type: Return, Expression: &expr}, err 232 | } 233 | 234 | func parseBreak(tokens []lexer.Token) (stmt Statement, err error) { 235 | if len(tokens) > 1 { 236 | return stmt, ErrInvalidStatement 237 | } 238 | 239 | return Statement{Type: Break}, err 240 | } 241 | 242 | func parseSkip(tokens []lexer.Token) (stmt Statement, err error) { 243 | if len(tokens) > 1 { 244 | return stmt, ErrInvalidStatement 245 | } 246 | 247 | return Statement{Type: Skip}, err 248 | } 249 | 250 | func parseExpression(tokens []lexer.Token) (stmt Statement, err error) { 251 | if len(tokens) == 0 { 252 | return Statement{Type: ExpressionStmt}, err 253 | } 254 | 255 | expr, err := expr.ParseExpression(tokens) 256 | return Statement{Type: ExpressionStmt, Expression: &expr}, err 257 | } 258 | 259 | // Else statements are consumed by the if parser so if one is found its an error 260 | func parseElse(tokens []lexer.Token) (stmt Statement, err error) { 261 | return stmt, ErrExpectedIf 262 | } 263 | 264 | func parsePrint(tokens []lexer.Token) (stmt Statement, err error) { 265 | if len(tokens) == 1 { 266 | return stmt, ErrExpectedExpression 267 | } 268 | 269 | expr, err := expr.ParseExpression(tokens[1:]) 270 | return Statement{Type: Print, Expression: &expr}, err 271 | } 272 | 273 | func parseError(tokens []lexer.Token) (stmt Statement, err error) { 274 | if len(tokens) == 1 { 275 | return stmt, ErrExpectedExpression 276 | } 277 | 278 | expr, err := expr.ParseExpression(tokens[1:]) 279 | return Statement{Type: Error, Expression: &expr}, err 280 | } 281 | 282 | // Also parses variable declaration with := operator 283 | func parseAssignment(tokens []lexer.Token) (stmt Statement, err error) { 284 | if len(tokens) < 3 { 285 | return parseExpression(tokens) 286 | } 287 | 288 | validOperands := []int{lexer.EQUAL, lexer.PLUS_EQUAL, lexer.MINUS_EQUAL, lexer.MULT_EQUAL, lexer.DIV_EQUAL, lexer.DEF_EQUAL} 289 | splits := util.SplitByTokens(tokens, validOperands) 290 | if len(splits) != 2 { 291 | return parseExpression(tokens) 292 | } 293 | 294 | left, err := expr.ParseExpression(splits[0]) 295 | if err != nil { 296 | return stmt, err 297 | } 298 | 299 | operator := tokens[len(splits[0])].Type 300 | right, err := expr.ParseExpression(splits[1]) 301 | return Statement{Type: Assignment, Expression: &right, Left: &left, Operator: operator}, err 302 | } 303 | 304 | func parseFunc(tokens []lexer.Token, idx *int) (stmt Statement, err error) { 305 | if len(tokens) < 6 { 306 | return stmt, ErrInvalidStatement 307 | } 308 | 309 | nameToken := tokens[*idx+1] 310 | if nameToken.Type != lexer.IDENTIFIER || tokens[*idx+2].Type != lexer.LEFT_PAREN { 311 | return stmt, ErrInvalidStatement // Missing identifier or block 312 | } 313 | 314 | *idx += 3 // Skip to start of param list 315 | endIdx, eof := seekToken(tokens, *idx, lexer.RIGHT_PAREN) 316 | if eof { 317 | return stmt, ErrInvalidStatement 318 | } 319 | 320 | // Get param names 321 | params := []string{} 322 | for _, p := range tokens[*idx:endIdx] { 323 | switch p.Type { 324 | case lexer.COMMA: 325 | continue 326 | case lexer.IDENTIFIER: 327 | params = append(params, p.Lexeme) 328 | continue 329 | } 330 | 331 | return stmt, ErrExpectedIdentifier 332 | } 333 | 334 | *idx = endIdx + 1 // Skip to start of block 335 | if tokens[*idx].Type != lexer.LEFT_BRACE { 336 | return stmt, ErrExpectedBlock 337 | } 338 | 339 | block, err := getBlockStatement(tokens, idx) 340 | return Statement{Type: Function, Name: nameToken.Lexeme, Params: params, Then: &block}, err 341 | } 342 | 343 | // Modifies index to go to block end. First token must be left brace 344 | func getBlockStatement(tokens []lexer.Token, idx *int) (block Statement, err error) { 345 | start := *idx 346 | if tokens[start].Type != lexer.LEFT_BRACE { 347 | return block, ErrExpectedBlock 348 | } 349 | 350 | if endIdx, eof := util.SeekClosingBracket(tokens, *idx, lexer.LEFT_BRACE, lexer.RIGHT_BRACE); !eof { 351 | *idx = endIdx 352 | blockTokens := tokens[start+1 : *idx] 353 | statements, err := ParseStatements(blockTokens) 354 | return Statement{Type: Block, Statements: statements}, err 355 | } 356 | 357 | return block, ErrNoBrace 358 | } 359 | 360 | func parseBlock(tokens []lexer.Token, idx *int) (stmt Statement, err error) { 361 | return getBlockStatement(tokens, idx) 362 | } 363 | 364 | // Parses expression and block after keyword 365 | func getExpressionAndBlock(tokens []lexer.Token, idx *int, expectExpr bool) (expr Statement, block Statement, err error) { 366 | startBlock, eof := seekToken(tokens, *idx, lexer.LEFT_BRACE) 367 | if eof { 368 | return expr, block, ErrExpectedBlock 369 | } 370 | 371 | expr, err = parseExpression(tokens[*idx+1 : startBlock]) 372 | if err != nil { 373 | return expr, block, err 374 | } 375 | 376 | if expectExpr && expr.Expression == nil { 377 | return expr, block, ErrExpectedExpression 378 | } 379 | 380 | *idx = startBlock 381 | block, err = getBlockStatement(tokens, idx) 382 | return expr, block, err 383 | } 384 | 385 | func parseIf(tokens []lexer.Token, idx *int) (stmt Statement, err error) { 386 | stmt, block, err := getExpressionAndBlock(tokens, idx, true) 387 | if err != nil { 388 | return stmt, err 389 | } 390 | 391 | // Check for else statement 392 | if *idx+1 < len(tokens) && tokens[*idx+1].Type == lexer.ELSE { 393 | if *idx+2 >= len(tokens) { 394 | return stmt, ErrExpectedBlock 395 | } 396 | 397 | *idx += 2 // Skip to block 398 | elseBlock, err := getBlockStatement(tokens, idx) 399 | return Statement{Type: If, Expression: stmt.Expression, Then: &block, Else: &elseBlock}, err 400 | } 401 | 402 | return Statement{Type: If, Expression: stmt.Expression, Then: &block}, err 403 | } 404 | 405 | func parseWhile(tokens []lexer.Token, idx *int) (stmt Statement, err error) { 406 | stmt, block, err := getExpressionAndBlock(tokens, idx, false) 407 | if err != nil { 408 | return stmt, err 409 | } 410 | 411 | if stmt.Expression == nil { 412 | return Statement{Type: While, Then: &block}, err 413 | } 414 | 415 | return Statement{Type: While, Expression: stmt.Expression, Then: &block}, err 416 | } 417 | 418 | func parseRepeat(tokens []lexer.Token, idx *int) (stmt Statement, err error) { 419 | stmt, block, err := getExpressionAndBlock(tokens, idx, true) 420 | if err != nil { 421 | return stmt, err 422 | } 423 | 424 | return Statement{Type: Repeat, Expression: stmt.Expression, Then: &block}, err 425 | } 426 | 427 | func parseObject(tokens []lexer.Token, idx *int) (stmt Statement, err error) { 428 | if len(tokens[*idx:]) < 4 { 429 | return stmt, ErrInvalidStatement 430 | } 431 | 432 | nameToken := tokens[*idx+1] 433 | if nameToken.Type != lexer.IDENTIFIER { 434 | return stmt, ErrExpectedIdentifier 435 | } 436 | 437 | *idx += 3 // Goto start of block 438 | if tokens[*idx-1].Type != lexer.LEFT_BRACE { 439 | return stmt, ErrExpectedBlock 440 | } 441 | 442 | endIdx, eof := seekToken(tokens, *idx, lexer.RIGHT_BRACE) 443 | if eof { 444 | return stmt, ErrNoBrace 445 | } 446 | 447 | fields := tokens[*idx:endIdx] 448 | fieldNames := []string{} 449 | for _, field := range fields { 450 | switch field.Type { 451 | case lexer.COMMA: 452 | continue 453 | case lexer.IDENTIFIER: 454 | fieldNames = append(fieldNames, field.Lexeme) 455 | continue 456 | } 457 | 458 | return stmt, ErrExpectedIdentifier 459 | } 460 | 461 | if len(fieldNames) == 0 { 462 | return stmt, ErrExpectedIdentifier 463 | } 464 | 465 | *idx = endIdx 466 | return Statement{Type: Object, Name: nameToken.Lexeme, Params: fieldNames}, err 467 | } 468 | -------------------------------------------------------------------------------- /stmt/stmt.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/jesperkha/Fizz/expr" 7 | ) 8 | 9 | var ( 10 | ErrNoSemicolon = errors.New("expected ; after statement, line %d") 11 | ErrInvalidStmtType = errors.New("invalid statement type, check statement parsing") 12 | ErrExpectedExpression = errors.New("expected expression in statement, line %d") 13 | ErrNoStatement = errors.New("expected statement before semicolon, line %d") 14 | ErrExpectedIdentifier = errors.New("expected identifier, line %d") 15 | ErrInvalidStatement = errors.New("invalid statement, line %d") 16 | ErrNoBrace = errors.New("expected } after block statement, line %d") 17 | ErrExpectedBlock = errors.New("expected block after statememt, line %d") 18 | ErrExpectedIf = errors.New("expected if statement before else, line %d") 19 | ErrInvalidOperator = errors.New("invalid statement operator, line %d") 20 | ErrDifferentTypes = errors.New("different types in statement, line %d") 21 | ErrNonCallable = errors.New("cannot call non-callable type, line %d") 22 | ErrCommaError = errors.New("comma error, line %d") 23 | ErrNonAssignable = errors.New("cannot assign value to non-subscriptable, line %d") 24 | ErrExpectedName = errors.New("expected filename at import, line %d") 25 | ErrCannotImport = errors.New("cannot import outside of global scope, line %d") 26 | ErrExpectedInteger = errors.New("expected expression to be integer, line %d") 27 | ErrExpectedNumber = errors.New("expected expression to be number, line %d") 28 | ErrInfiniteLoop = errors.New("infinite loop in range statement not allowed, line %d") 29 | ErrMaximumRecursion = errors.New("maximum recursion depth exceeded, line %d") 30 | ErrProgramExit = errors.New("") 31 | 32 | ErrReturnOutsideFunc = ConditionalError{Msg: "cannot use return outside of a function, line %d", Type: RETURN} 33 | ErrSkipOutsideLoop = ConditionalError{Msg: "cannot use skip outside of a loop, line %d", Type: SKIP} 34 | ErrBeakOutsideLoop = ConditionalError{Msg: "cannot use break outside of a loop, line %d", Type: BREAK} 35 | ) 36 | 37 | const ( 38 | NotStatement = iota 39 | ExpressionStmt 40 | Print 41 | Variable 42 | Assignment 43 | Block 44 | If 45 | While 46 | Repeat 47 | Break 48 | Skip 49 | Function 50 | Return 51 | Exit 52 | Object 53 | Import 54 | Include 55 | Error 56 | Enum 57 | Range 58 | ) 59 | 60 | type Statement struct { 61 | Type int 62 | Line int 63 | Operator int 64 | Name string 65 | Params []string 66 | Statements []Statement 67 | Then *Statement 68 | Else *Statement 69 | Expression *expr.Expression 70 | Left *expr.Expression 71 | } 72 | 73 | const ( 74 | SKIP = iota 75 | BREAK 76 | RETURN 77 | ) 78 | 79 | type ConditionalError struct { 80 | Type int 81 | Line int 82 | Msg string 83 | Value interface{} 84 | } 85 | 86 | func (c ConditionalError) Error() string { 87 | return c.Msg 88 | } 89 | -------------------------------------------------------------------------------- /term/cmd.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | var CommandDescriptions = map[string]string{ 4 | "docs": "use 'fizz docs [library name]' to print out a full list of functons declared in that library. example: 'fizz docs io'", 5 | "help": "prints a longer description for a given command. example 'fizz help docs'", 6 | } 7 | -------------------------------------------------------------------------------- /term/handler.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | // Implementation of the ArgList interface 4 | 5 | type ArgHandler struct { 6 | flags []string 7 | options []string 8 | args []string 9 | subcmd string 10 | } 11 | 12 | func (a *ArgHandler) HasFlag(flag string) bool { 13 | for _, f := range a.flags { 14 | if f == flag { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func (a *ArgHandler) HasOption(option string) bool { 22 | for _, o := range a.options { 23 | if o == option { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | 30 | func (a *ArgHandler) SubCommand() string { 31 | return a.subcmd 32 | } 33 | 34 | func (a *ArgHandler) Args() []string { 35 | return a.args 36 | } 37 | -------------------------------------------------------------------------------- /term/help.txt: -------------------------------------------------------------------------------- 1 | USE: 2 | fizz 3 | fizz [filename] 4 | fizz [flags] [filename] 5 | fizz [command] [args] 6 | 7 | FLAGS: 8 | -e print global env after finish 9 | -f print function callstack with errors 10 | 11 | --help what you are reading now 12 | --version print fizz version 13 | 14 | COMMANDS: 15 | help [command] 16 | displays more information about a given command 17 | 18 | docs [lib name] 19 | prints out all functions declared in specified library -------------------------------------------------------------------------------- /term/parser.go: -------------------------------------------------------------------------------- 1 | package term 2 | 3 | import ( 4 | _ "embed" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/jesperkha/Fizz/util" 11 | ) 12 | 13 | // Terminal argument parser 14 | 15 | var ( 16 | //go:embed help.txt 17 | HELP string 18 | 19 | ErrUnknownOption = errors.New("uknown option '%s'") 20 | ErrUnknownCommand = errors.New("unknown command '%s'") 21 | ) 22 | 23 | type ArgList interface { 24 | // Returns true if flag is present (argument starting with '-') 25 | HasFlag(flag string) bool 26 | 27 | // Returns true if option is present (argument starting with '--') 28 | HasOption(option string) bool 29 | 30 | // Returns the name of the subcommand used. The subcommand is the first 31 | // string found after the program name, unless it is the only argument, 32 | // in which case it will be handled as an argument, not a subommand. 33 | SubCommand() string 34 | 35 | // Returns the arguments, not flags, options, or the subcommand. 36 | Args() []string 37 | } 38 | 39 | // Parses arguments into ArgList. Raises error if an unknown flag, option, or 40 | // subcommand is found. 41 | func Parse(valid []string) (list ArgList, err error) { 42 | args := os.Args[1:] 43 | handler := ArgHandler{} 44 | 45 | for idx, arg := range args { 46 | if strings.HasPrefix(arg, "--") { 47 | // Check if option 48 | if !util.SContains(valid, arg) { 49 | return list, fmt.Errorf(ErrUnknownOption.Error(), arg) 50 | } 51 | handler.options = append(handler.options, strings.TrimLeft(arg, "-")) 52 | } else if strings.HasPrefix(arg, "-") { 53 | // Check if flag (after to avoid false positive) 54 | if !util.SContains(valid, arg) { 55 | return list, fmt.Errorf(ErrUnknownOption.Error(), arg) 56 | } 57 | handler.flags = append(handler.flags, strings.TrimLeft(arg, "-")) 58 | } else if idx == 0 && len(args) != 1 { 59 | // Check for valid subcommand 60 | handler.subcmd = arg 61 | } else { 62 | handler.args = append(handler.args, arg) 63 | } 64 | } 65 | 66 | return &handler, err 67 | } 68 | -------------------------------------------------------------------------------- /test/cases/invalid_expr.fizz: -------------------------------------------------------------------------------- 1 | +1 - 8; 2 | () * 5; 3 | x + 1; 4 | 1.0.2 - 1; 5 | 1 --+ 1; 6 | 2 + +; 7 | 1 + true; 8 | "2" + 2; 9 | ()); 10 | ((); 11 | [1, ]; 12 | [1, 2, 3][4]; 13 | (2)[0]; 14 | ([)]; 15 | "hello"[6]; 16 | 1 in 1; -------------------------------------------------------------------------------- /test/cases/invalid_stmt.fizz: -------------------------------------------------------------------------------- 1 | # Variables and values 2 | prt 30; 3 | a a = 0; 4 | c := p(1); c.n.n; 5 | b.n(); 6 | .b.n; 7 | b.n.; 8 | [1, 2, 3][0][0] = 4; 9 | (1, 2, 3)[0] = 4; 10 | enum { 1 two } 11 | 12 | # Functions 13 | func(){} 14 | func main(a) {} main(); 15 | 16 | # Conditionals and loops 17 | if {} 18 | while a + 2 {} 19 | repeat (1, 2) {} 20 | range 2 in 10 {} 21 | range 1, 2, 3, 4 {} 22 | range 0, 10, -1 {} -------------------------------------------------------------------------------- /test/cases/valid_expr.fizz: -------------------------------------------------------------------------------- 1 | 1 + 1; 2 | (5 + 1) * 2; 3 | -1 + -1; 4 | -5 + 5 - 5; 5 | 8.2 % 2; 6 | type true == "bool"; 7 | 5 - ((2^3) + 2); 8 | [1, 2, 3][1 + 1]; 9 | ([1, 2, 3][0]) + [3, 2, 1][1]; 10 | func f() {return 1;} ([["hello"], 2, 3][f() - [1, 2, 3][0]])[0]; 11 | 1 in [1, 2, 3]; -------------------------------------------------------------------------------- /test/cases/valid_stmt.fizz: -------------------------------------------------------------------------------- 1 | # Expression statements 2 | type 1; 3 | 4 | # Assignment 5 | a := 20; 6 | a := 0; a += 1; 7 | define object{n} a := object(2); a.n = 2; 8 | define object{n} a := object(object(object(1))); a.n.n.n = 1; 9 | [1, 2, 3][0] = 4; 10 | ([1, 2, 3])[0] = 4; 11 | [[1, 2, 3]][0][0] = 4; 12 | 13 | # Conditionals and loops 14 | if true { 1 + 1; } 15 | while { break; } 16 | repeat (20 - 1) {} 17 | arr := [2, 3, 4]; range n in arr {} 18 | range n in 5 - 1 {} 19 | range n in (5 - (4 + 1)), 1 {} 20 | range n in 3, (5 + 5), 3 {} 21 | 22 | # Functions 23 | func main(a, b) { return 1; } main(1, 2); 24 | func closure() { i := 0; func add() {i += 1;} return add; } f := closure(); f(); 25 | 26 | # Other 27 | enum { one two three } 1 + one; 28 | include "str"; str.toString(1); 29 | -------------------------------------------------------------------------------- /test/fizz_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/jesperkha/Fizz/interp" 11 | ) 12 | 13 | const ( 14 | validCaseFile = true 15 | invalidCaseFile = false 16 | ) 17 | 18 | var ( 19 | testFiles = []string{ 20 | "stmt", 21 | "expr", 22 | } 23 | ) 24 | 25 | func TestAll(t *testing.T) { 26 | for i := float64(0); int(i) < len(testFiles); i += 0.5 { 27 | testType := float64(int(i)) == i 28 | invalid := "" 29 | if testType == invalidCaseFile { 30 | invalid = "in" 31 | } 32 | 33 | name := testFiles[int(math.Floor(i))] 34 | filename := fmt.Sprintf("./cases/%svalid_%s.fizz", invalid, name) 35 | byt, err := os.ReadFile(filename) 36 | if err != nil { 37 | t.Error(err) 38 | } 39 | 40 | cases := strings.Split(string(byt), "\n") 41 | for idx, c := range cases { 42 | c = strings.TrimSpace(c) // Removes invisible characters 43 | if c == "" || strings.HasPrefix(c, "#") { 44 | continue // Skip comments and whitespace for invalid case files 45 | } 46 | _, err := interp.Interperate("", c) 47 | 48 | // Valid case first 49 | if err != nil && testType == validCaseFile { 50 | // Case number is line number in case file 51 | t.Errorf("%s, valid case %d got error: %s", filename, idx+1, err) 52 | } 53 | 54 | // Invalid cases 55 | if err == nil && testType == invalidCaseFile { 56 | t.Errorf("%s, invalid case %d got no error: %s", filename, idx+1, c) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "strings" 8 | 9 | ct "github.com/daviddengcn/go-colortext" 10 | "github.com/jesperkha/Fizz/env" 11 | "github.com/jesperkha/Fizz/lexer" 12 | ) 13 | 14 | // Format error with line numbers for local errors, but ignore for errors passed from 15 | // expression parsing as they are already formatted with line numbers. 16 | func FormatError(err error, line int) error { 17 | if err == nil { 18 | return err 19 | } 20 | 21 | if strings.Contains(err.Error(), "%d") { 22 | return fmt.Errorf(err.Error(), line) 23 | } 24 | 25 | return err 26 | } 27 | 28 | // Converts value to string in proper representation format 29 | func FormatPrintValue(val interface{}) string { 30 | switch val.(type) { 31 | case float64, string, bool: 32 | return fmt.Sprint(val) 33 | case nil: 34 | return "nil" 35 | } 36 | 37 | if e, ok := val.(env.Environment); ok { 38 | glob := e[0] 39 | total := "" 40 | for k, v := range glob { 41 | total += fmt.Sprintf("%s: %s\n", k, FormatPrintValue(v)) 42 | } 43 | 44 | return total 45 | } 46 | 47 | if o, ok := val.(*env.Object); ok { 48 | str := o.Name + ": {\n" 49 | for key, value := range o.Fields { 50 | str += fmt.Sprintf(" %s: %v\n", key, FormatPrintValue(value)) 51 | } 52 | 53 | return str + "}" 54 | } 55 | 56 | if o, ok := val.(*env.Callable); ok { 57 | return o.Name + "()" 58 | } 59 | 60 | if a, ok := val.(*env.Array); ok { 61 | str := "[" 62 | for i, v := range a.Values { 63 | if i != 0 { 64 | str += ", " 65 | } 66 | 67 | str += fmt.Sprintf("%v", FormatPrintValue(v)) 68 | } 69 | 70 | return str + "]" 71 | } 72 | 73 | return "" 74 | } 75 | 76 | // Prints red error message to console 77 | func PrintError(err error) { 78 | if err == nil { 79 | return 80 | } 81 | 82 | ct.Foreground(ct.Red, true) 83 | fmt.Fprintln(os.Stderr, err.Error()) 84 | ct.ResetColor() 85 | } 86 | 87 | // Prints error followed by program exit. Exit code 1 is reserved for crashes 88 | func ErrorAndExit(err error) { 89 | PrintError(err) 90 | os.Exit(1) 91 | } 92 | 93 | // Prints msg and exits with code 0 94 | func PrintAndExit(msg interface{}) { 95 | fmt.Println(msg) 96 | os.Exit(1) 97 | } 98 | 99 | // Checks if tokens is in tokenlist 100 | func Contains(arr []int, target int) bool { 101 | for _, v := range arr { 102 | if v == target { 103 | return true 104 | } 105 | } 106 | 107 | return false 108 | } 109 | 110 | func SContains(arr []string, target string) bool { 111 | for _, v := range arr { 112 | if v == target { 113 | return true 114 | } 115 | } 116 | 117 | return false 118 | } 119 | 120 | // Returns index of last token 121 | func SeekClosingBracket(tokens []lexer.Token, start int, beginT, endT int) (endIdx int, eof bool) { 122 | numParen := 0 123 | for i := start; i < len(tokens); i++ { 124 | switch tokens[i].Type { 125 | case beginT: 126 | numParen++ 127 | case endT: 128 | numParen-- 129 | } 130 | 131 | if numParen == 0 { 132 | return i, false 133 | } 134 | } 135 | 136 | return endIdx, true 137 | } 138 | 139 | // Skips token check if in group or array expression. Returns index of ending token or eof. 140 | func SeekBreakPoint(tokens []lexer.Token, verifier func(int, lexer.Token) bool) (targetIdx int, eof bool) { 141 | parens := 0 142 | targetIdx = -1 143 | for idx, token := range tokens { 144 | // Check before to allow for seeking parens 145 | if parens == 0 && verifier(idx, token) { 146 | targetIdx = idx 147 | } 148 | 149 | switch token.Type { 150 | case lexer.LEFT_PAREN, lexer.LEFT_SQUARE: 151 | parens++ 152 | case lexer.RIGHT_PAREN, lexer.RIGHT_SQUARE: 153 | parens-- 154 | } 155 | } 156 | 157 | return targetIdx, targetIdx == -1 158 | } 159 | 160 | // Splits list of token by split type 161 | func SplitByToken(tokens []lexer.Token, split int) [][]lexer.Token { 162 | return SplitByTokens(tokens, []int{split}) 163 | } 164 | 165 | // Splits list of token by multiple split types 166 | func SplitByTokens(tokens []lexer.Token, splits []int) [][]lexer.Token { 167 | numParen := 0 168 | start := 0 169 | result := [][]lexer.Token{} 170 | for idx, token := range tokens { 171 | switch token.Type { 172 | case lexer.LEFT_PAREN, lexer.LEFT_SQUARE: 173 | numParen++ 174 | case lexer.RIGHT_PAREN, lexer.RIGHT_SQUARE: 175 | numParen-- 176 | } 177 | 178 | if Contains(splits, token.Type) && numParen == 0 { 179 | result = append(result, tokens[start:idx]) 180 | start = idx + 1 181 | } 182 | } 183 | 184 | // Append last item 185 | if len(tokens) != 0 { 186 | result = append(result, tokens[start:]) 187 | } 188 | 189 | return result 190 | } 191 | 192 | // Returns Fizz name for value 193 | func GetType(value interface{}) string { 194 | if i, ok := value.(env.FizzObject); ok { 195 | return (i).Type() 196 | } 197 | 198 | switch value.(type) { 199 | case float64, float32, int: 200 | return "number" 201 | case nil: 202 | return "nil" 203 | } 204 | 205 | return reflect.TypeOf(value).Name() 206 | } 207 | 208 | // Gets fizz type name from reflect name 209 | func GetLibType(typ string) string { 210 | switch typ { 211 | case "Object": 212 | return "object" 213 | case "Callable": 214 | return "function" 215 | case "Array": 216 | return "array" 217 | } 218 | 219 | return typ 220 | } 221 | 222 | // Adds filename to error message if not already done. Returns nil if err is nil. 223 | func WrapFilename(filename string, err error) error { 224 | if err == nil || err.Error() == "" { 225 | return err 226 | } 227 | 228 | if !strings.Contains(err.Error(), ".fizz") { 229 | err = fmt.Errorf("%s: %s", filename, err.Error()) 230 | } 231 | 232 | return err 233 | } 234 | 235 | type pair struct{ a, b string } 236 | type UniquePairs map[pair]bool 237 | 238 | // Returns true if pair is already present in map 239 | func (u UniquePairs) Add(a, b string) bool { 240 | if a > b { 241 | a, b = b, a 242 | } 243 | 244 | p := pair{a, b} 245 | n := u[p] 246 | u[p] = true 247 | return n 248 | } 249 | 250 | // Removes any path / file extension noise from filename 251 | func GetPlainFilename(path string) string { 252 | path = strings.TrimSuffix(path, ".fizz") 253 | split := strings.Split(path, "/") 254 | return split[len(split)-1] 255 | } 256 | 257 | func IsInt(value interface{}) (int, bool) { 258 | if v, ok := value.(float64); ok { 259 | iv := int(v) 260 | return iv, v == float64(iv) 261 | } 262 | 263 | return -1, false 264 | } 265 | --------------------------------------------------------------------------------