├── .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 |
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 |
--------------------------------------------------------------------------------