An HTML version of this file is in the `readme` folder :) # Commands and Tutorial ``` Author........Zeda Thomas e-mail........xedaelnara@gmail.com Project.......Grammer Version.......2.50.7.1 (I will probably forget to change this :( ) Last Update...7 November 2019 Language......English Programming...Assembly Size..........2-Page app ``` Hey! You! This app is still in development! If you have issues, please send me an email, post in the forums, or, post an issue on GitHub! Thanks, you're the best :) To follow the progress of Grammer, check out [Omnimaga](https://www.omnimaga.org/grammer/), [Cemetech](http://www.cemetech.net/forum/viewforum.php?f=71) or [GitHub](https://github.com/Zeda/Grammer2), where development is most active. If you have questions or suggestions, feel free to email me or post in the forums!
> :End`, no matter what `<>` does to A,B, and C, they will be restored when the `End` is parsed.
### Examples With Blocks
```
:If A=B ;Since A=B is false, the following line is skipped
:9→A
```
Or also:
```
:If 3=B→A:*14:-14 ;This is the full statement
:Text(0,0,"Yay!
```
```
:If 3=4
:Then
:3→A
:9→B
:16→C
:End
```
```
:If 3=4
:Then
:3→A
:9→B
:Else
:16→C ;This is the code that gets executed
:End
```
```
:Repeat getKey=15
:End
```
`!Repeat ` checks if the statement is false in order to end. For example,
to remain in the loop while Enter is being pressed:
```
:!Repeat getkey=9
:End
```
```
:For(R,0,48
:Circle(32,48,R,1
:DispGraph
:End
:Stop
```
```
:0→A→B
:While getKey≠15
:A+1→A
:B-1→B
:End ;This tells the While loop to End / restart!
```
An example with `▶Nom(`
```
:ClrDraw
:▶Nom(A,B
:0→B
:For(A,0,9
:B+A→B
:Text('0,0,A
:Text('6,0,B
:DispGraph
:Pause 33
:End
:End
```
## Control
| Name | Description |
|:------------ |:------- |
| Return | This returns a pointer to the next line of code.
| Goto | This is unlike the BASIC `Goto` command. This jumps to a pointer as opposed to a label.
| Lbl | This returns the pointer of a label. The argument is a pointer to the label name. For example, `Lbl "HI` will search for `.HI` in the program code. Also, you can specify which variable the label is in. For example, if you wanted to jump to a label in another program, you can add a second argument as the name of the var. For example, to find the label `HI` in prgmBYE: `Lbl "HI","BYE` ***Note:*** *If your label moves, you'll need to manually update the pointer! For example, when creating a variable, or resizing one, or archiving/unarchiving. I recommend doing all of that first, then finding labels. If your label moves, then you might accidentally execute garbage data.*
| Pause xx | This will pause for approximately xx/100 seconds. So `Pause 66` will pause for about .66 seconds.
| Pause | This will wait for [Enter] to be pressed and released.
| `prgm` | This is used to execute a sub routine.
| `prgm(` | This executes a subroutine, and sets `?` var to to point to the arguments. For example, `prgm(Z,1,2,3` will call the routine that `Z` points to, and sets `?` to point to the `1`.
| `]` | This takes a pvar as an argument. It parses the code at the pointer as an argument, then updates the pointer to point to the next argument. This is useful for parsing arguments passed to a subroutine. Take the example above, if the subroutine that `Z` points to has the code `]?`, then the `1` will be parsed. The next `]?` will parse the `2`, and so on.
| Func | The arguments are `FuncPointer[,Counter`. This will automatically execute the subroutine pointed to by <> based on Counter. Counter is based on an internal counter, not based on actual timings like seconds or milliseconds. The default is 128. See the [examples](#control-examples) below.
| Asm( | This can be used to run an assembly program.
| AsmPrgm | This allows you to input an asm code in hex. (C9 is needed)
| ln( | This will let you jump forward or backward a given number of lines.
| L | The list L. Arguments are `L``line#,[start,[size,[EOL` This let's you execute a specific line number. By default, it starts the line count within the main program, but you can pass an optional start value, an optional size value (default is 32768 bytes long), and an optional End-Of-Line argument (default is 63, the newline token).
| Param | `?` points to parameters, this stores those parameter values to variables. For example, if `?` points to `1,2,3,4`, then `ParamA,B,C,D` will store `1` to `A`, `2` to `B`, `3` to `C`, and `4` to `D`. This updates `?`. This is useful for subroutines that take parameters! See the example below.
| Param' | This pushes values to the parameter stack. For example, `Param'A,0,1,B+2` pushes the value of A, 0, 1, and B+2 to the stack.
| Param° | This pops values off the parameter stack into a var. For example, using the previous `push` sequence, `Param°A,B,C,D` would store the original `B`+2 to `A`, `1` to `B`, `0` to `C`, and the original `A` to `D`.
| Pmt_Bgn | This token is located at [Apps][Finance][Up]. This is a var that holds the base location for the parameter stack. Changing this value automatically resets the parameter stack pointer.
| Pmt_End | This token is located at [Apps][Finance][Up][Up]. This is a var that holds the end location for the parameter stack.
| `▶Nom(`x,y,z | This starts a block in which a defined list of variables is preserved. For example, in `▶Nom(A,B,C: <> :End`, no matter what `<>` does to A,B, and C, they will be restored when the `End` is parsed.
### Control Examples
```
:Return→L
:<>
:Goto L ;This jumps to the line after "Return→L"
```
An example with setting an interrupt
```
:FuncLbl "DISP
:Repeat getKey(15
:<>
:End
:Stop
:.DISP
:DispGraph
:End
```
That will do DispGraph several times per second automatically.
An example with `▶Nom(`
```
:ClrDraw
:▶Nom(A,B
:0→B
:For(A,0,9
:B+A→B
:Text('0,0,A
:Text('6,0,B
:DispGraph
:Pause 33
:End
:End
```
Jump three lines with `ln(`
```
:ln(3
:"NOT
:"Executed
:"YAY :D
```
Or to jump backwards:
```
:"YAY :D
:"Erm...
:"Yeah...:ln(-3
```
```
:ClrDraw
:Lbl "BoldLine(→Z
:prgm(Z,rand,rand,rand,rand,1
:DispGraph
:Stop
:.BoldLine(
:ParamA,B,C,D,E
:Line('A,B,C,D,E
:Line('A+1,B,C+1,D,E
:Line('A,B+1,C,D+1,E
:End
```
## Input/Computing
| Name | Description |
|:------------ |:------- |
| getKey | This returns a value from 0 to 56 that is the current key press. You can use [this chart](#keycodes) for values.
| getKey( | `getKey(` will allow you to see if a key is being pressed. For example, `getKey(9` will return `1` if enter is pressed
| Input | This allows you to input a string. The pointer to the string is returned. (this is not a permanent location, the data will be overwritten the next time Input is used). To get a value input from the user, you can use `expr(` : `expr(Input →A`. This will store the result to A. `Input` can also take an optional string input. The input string will be displayed after what the user is typing. If you execute this code, I think it'll explain it better. It's honestly pretty cool for a calculator. **See below for information on the [Input vars!](#input-vars)**
| Menu( | ~~*This may require the included appvar, GramPkg, to be on your calc (in RAM or archived).*~~ Syntax is, `Menu(y,x,w,"Title","Item0","Item1","Item2","Exit`. It basically makes a pop-up style menu, returning the number of the selected item. Returns 0 if exited due to `[CLEAR]` or `[ON]`. ***Note:** you can append an optional last argument that starts with `'` to specify a default option. For example, if the last argument is `'3` then the third option will be selected by default.*
| Menu(' | Draws a menu that queries Grammer subroutines for the content to render. Syntax is `Menu('"Title",y,x,height,width,GET_ELEMENT_ptr,SELECT_ELEMENT_ptr`. The subroutine for GET_ELEMENT will receive the index in Ans. Return 0 if out-of-bounds, else return a pointer to the string to draw. The subroutine for SELECT_ELEMENT will receive the index in Ans. Modify this as you want, the result will be returned as the result of the menu. Returns 0 if exited due to `[CLEAR]` or `[ON]`. |
| Ans | This will return the value of the previous line.
| expr( | This will compute a string as a line of code (useful with `Input`). **See below for more info on [`expr(`](#expr-examples)!**
| inString( | This is similar to the TI-BASIC command. This will return the location of a sub-string. The inputs are where to start searching and the string to search for: `inString(SearchStart,SearchString[,maxlength]`. The size of the input string is returned in `Ɵ'` and if there was no match found, 0 is returned.
| length( | This will return the size of a variable (in RAM or Archive) as well as the pointer to the data in `Ɵ'`. For example, to get the size of the appvar named `Data`: `length("UData→A`. If the var is not found, -1 is returned.
| length(' | This is used to search for a line. For example, if you want to find a specific line number in a program, this is what you would use. The syntax: `length('StartSearch,Size,LineNumber,[LineByte`, `StartSearch` is where to begin the search `Size` is how many bytes to search in. 0 will search all RAM. `LineNumber` is the line number you are looking for. `LineByte` is an optional argument for what byte is considered a new line. The output is the location of the string and `Ɵ'` has the size of the string. If the line is not found, the last line is returned instead.
Here is an example with the basic menu:
```
Menu(1,1,16,"Title","ITEM 1","ITEM 2","ITEM 3→M
```
And here is an example using callbacks:
```
Lbl "GET→A
Lbl "SEL→B
Menu('"Title",2,33,59,30,A,B→M
Text('0,0,M
Stop
.GET
→X<26
If !
End
"ITEM A→Z
int(Z+5,X+65
Z
End
.SEL
+1
End
```
### Input Vars!
There are two Input variables that you can store to:
```
x→Input This sets the address of the Input buffer.
x→Input' This sets the size of the Input buffer. Remember, 1 byte is used to mark the end of the string!
```
### expr( Examples
`expr(` can be used to evaluate a line of Grammer code, usually from a string. For example:
```
expr("2+3→X
```
And this will set `X` to 5.
That's pretty boring, but it is handy if you want to parse user input:
```
expr(Input →X
```
***NOTE:** This will evaluate up to a newline (`Input` appends a newline to the user's input).
This means a user could enter any valid Grammer code, even if it messes with your program's variables!*
This note also means you might have some unexpected behavior. For example, let's try this:
```
"Rect(X,Y,15,15,2→Z
expr(Z
DispGraph
```
That inverts a 15-by-15 rectangle at (x,y), as you might have expected.
![*Graph screen, with the center 15-by-15 pixels inverted*](https://i.imgur.com/pxvzqNF.png)
Now let's try to invert that twice:
```
"Rect(X,Y,15,15,2→Z
expr(Z
DispGraph
expr(Z
DispGraph
```
![*Egads! An error is thrown!*](https://i.imgur.com/K96TsgX.png)
An error is thrown! Why is this? Well, this will teach something about the internals of the
Grammer parser. The `"` token basically returns a pointer to the next token. So the first line
causes `Z` to point to the `Rect(` token:
```
"Rect(X,Y,15,15,2→Z
^
Z literally points here
```
Since `expr(` parses up to a newline, and we give it the pointer `Z`, `expr(` executes:
`Rect(X,Y,15,15,2→Z` instead of `Rect(X,Y,15,15,2` as you might expect. So this means
`Z` is modified by the first `expr(`, so the second `expr(` is trying to read some random
piece of memory as Grammer code. Ouch!
So the simple fix is to put the `→Z` on the next line:
```
"Rect(X,Y,15,15,2
→Z
expr(Z
DispGraph
expr(Z
DispGraph
```
And now it inverts and then reinverts. Ta-da!
### Useful expr( trick
Here is a useful trick with `expr(`. Suppose we want to invert a sprite on two buffers (grayscale?)
display it, then re-invert it (this is useful for showing a sprite, without destroying the graphics
buffer).
We could do something like:
```
Pt-Off(2,S,Y,X
Pt-Off(2,S,Y,X,1,8,B
DispGraph
Pt-Off(2,S,Y,X
Pt-Off(2,S,Y,X,1,8,B
```
But by combining the `Return` command with `expr(`, we can do this:
```
Return→Z
Pt-Off(2,S,Y,X:Pt-Off(2,S,Y,X,1,8,B
DispGraph
expr(Z
```
### Input Examples
```
.0:Return
ClrDraw
Text(°"(x,y)=( ;ClrDraw sets the cursor to (0,0), so I can use °
expr(Input ",)→X ;I get the next input here. The string is ,)
Text(,+1 ;This increments the X coordinate.
expr(Input ")→Y ;This gets the Y value.
Pxl-On(Y,X ;Or whatever you want to do with the coordinates.
DispGraph
Stop
```
`inString(` example.
```
:Lbl "DATA→A
:inString(A,"How→B
:.DATA
:HELLOHowdyWoRlD!
```
## solve(
This is a command subset. Commands start as `solve(#,`
| `#` | Name | Description |
|:--- |:------------ |:------- |
| 0 | CopyVar | `solve(0,"VarName1","VarName2"[,size[,offset`. This will copy the program named by VarName1 from RAM or archive to a new program named by VarName2. If Varname2 already exists, it will be overwritten. So for example, to copy Str6 to Str7: `solve(0,"DStr6","DStr7`. This returns the pointer to the new var and the size of the var is in Ɵ'. The last arguments are optional. Size lets you choose how many bytes are copied (instead of just copying the whole var). You can also add an offset argument to tell where to start reading from.
| 1 | CopyDataI | `solve(1,loc i ,loc f ,size`. This copies data from `loc i` to `loc f` . (Forward direction)
| 2 | CopyDataD | `solve(2,loc i ,loc f ,size`. This copies data from `loc i` to `loc f` . (Backward direction)
| 3 | ErrorHandle | `solve(3,Pointer`. This will allow your program to have a custom error handler. Pointer is 0 by default (meaning Grammer will handle it). Otherwise, set it to another value and Grammer will redirect the program to that location. The error code is returned in Ans. Ans and Ɵ' are put back to normal when the error handler completes. Errors: `0=ON`, `1=Memory`
| 4 | CallError | `solve(4,Error#`. This will execute the error code of a Grammer error. For example, to make a Memory error: `solve(4,1`. Using Error 2, you can input a string for a custom error: `solve(4,2,"Uh-Oh!`
| 5 | PortWrite( | `solve(5,port#,value`. This writes to a port. Ports give information about peripherals. You can find ports documentation at [WikiTi](http://wikiti.brandonw.net/index.php?title=Category:83Plus:Ports:By_Address)
| 6 | PortRead( | `solve(6,port#`. Reads a byte from the port.
| 7 | CopyVars | `solve(7,addr`. Copies the pointer vars to some other location. Currently this requires 108 bytes of space in the new buffer. Good for backing up vars.
| 8 | OverwriteVar | `solve(8,addr`. Overwrites pointer variables with new data.
Here is an error handler example
```
:solve(3,Lbl "ERR
:<>
:.ERR
:If =1 ;Means there was a memory error
:Stop
:End
```
## Physics
| Name | Description |
|:------------ |:------- |
| R▶Pr( | This will clear the particle buffer.
| R▶Pθ( | This will recalculate the particle positions and draw them. If you want to change the particle buffer, just add a pointer argument. `Get("EBUF→A:R▶Pθ(A-2`
| P▶Rx( | This will add a particle to the buffer. Just use the pixel coordinate position. For example: `P▶Rx(2,2`
| P▶Ry( | This will change the particle effect. `0` is normal sand, `1` is boiling, `2` lets you put in a basic custom rule set. If you want it to check Down, then Left/Right, then Up, use the following pattern: `0000 1000 0110 0001`2. That makes it first check down, and if it cannot go down, it then checks left or right, and if it cannot go left or right, it tests up. In decimal, that is 2145, so you would do: `P▶Ry(2,2145`. To make things easier, though, you can just use a string. This will achieve the same effect: `P▶Ry(2,"D,LR,U`. **Note** that you do need the actual string, not a pointer.
| P▶Rx(' | This will convert a rectangular region of the screen to particles. The inputs are `P▶Rx('Y,X,Height,Width`. This scans the area for pixels that are turned on and adds them to the current particle buffer.
## Miscellaneous
| Name | Description |
|:------------ |:------- |
| ▶DMS | Found in the angle menu, this is the "module" token. Modules allow you to extend Grammer's functionality. Grammer comes with a default module which must be included to use some functions (like the `Menu` command). Currently, you can have up to five other modules. For example, if you have a module packaged as an appvar called `MyModule`: `"5MyModule→▶DMS`. In order to execute a function `MyFunc(` from one of the modules, use : `▶DMSMyFunc`. If you have the token hook enabled (from Grammer's main menu), it looks a little cleaner: `"5MyModule→$` and `$MyFunc`, respectively.
| conj( | **Warning:** I have no knowledge of musical jargon, so excuse my mistakes. This is a sound command with three inputs. The syntax is `conj(Note,Octave,Duration`. Notes are: 0=C, 1=C#, 2=D, 3=D#, 4=E, 5=F, 6=F#, 7=G, 8=G#, 9=A, 10=A#, 11=B. Octave is 0 to 6. Duration is in 64th notes. So for example, a 32nd dot note uses 3/64th time. Duration is thus 3.
| conj(' | This sound routine has two different functions `conj('Duration,'Period` or `conj('Duration,DataLoc,Size`. This reads data for the period directly to save time (intead of converting numbers on the fly). Size is the size of the data in words, not bytes.
## Data Structures
Grammer doesn't really have any data structures which is both good and bad.
Bad because it makes you have to think a little more about how to approach a
problem, but good in that it allows you to create precisely what you need. This is
where you will need commands to create variables, insert or remove data, and edit
the data. I will also try to explain how to create some basic data structures like
arrays and matrices. First, here are the commands you have to work with:
## Memory Access
| Name | Description |
|:------------ |:------- |
| FindVar( *Get(* | This uses a string for the name of an OS var and returns a pointer to its data. If the variable does not exist, this returns 0. If it is archived, the value returned will be less than 32768. Ɵ' contains the flash page the variable is on, if it is archived, otherwise Ɵ' is 0. As an example, `Get("ESPRITES→A'` would return a pointer to the data of `prgmSPRITES` in `A'`.
| ( | Use this to read a byte of data from RAM
| { | Use this to read a two byte value from RAM (little endian)
| int( | Use this to write a byte of data to RAM.
| iPart( | Use this to write a word of data to RAM, little-endian (a word is 2 bytes). For example, to set the first two bytes to 0 in prgmHI: `Get("EHI→A:iPart(A,0`
| MakeVar( *Send(* | Use this to create Appvars or programs of any size and filled with zeros (so long as there is enough memory). For example, to create `prgmHI` with 768 bytes: `Send(768,"EHI`. Programs must be prefixed with `"E"`, protected programs `"F"` and appvars `"U"`. Lowercase letters are allowed! :)
| [ | Store a sequence of bytes to a given location. For example, `A[1,2,3,4` will store 4 bytes at A. You can also store some elements as two-byte words by using the `°` token. `A[1,2,3°,4`
| [[ | Stores a sequence of 2-byte words. `A[[1,2,3,4`
| [( | Stores hexadecimal input as raw data. `A[(3C7EFFFFFFFF7E3C`
| IS>( | This is used to read memory. The argument is one of the pointer vars. It reads the byte pointed to by the pvar and then the pvar is incremented (so consecutive uses will read consecutive bytes).
| DS<( | This is used to read memory. The argument is one of the pointer vars. It reads the byte pointed to by the pvar and then the pvar is decremented (see note on `IS>(`)
| Archive | Follow this with a var name to archive the var. For example, to archive `prgmPROG`, do this: `Archive "EPROG`.
| Unarchive | Use this like `Archive`, except this unarchives the var
| Delvar | Use this like `Archive`, except this will delete a var
| sub( | Use this to remove data from a variable. the syntax is: `sub(#ofBytes,Offset,"Varname`. For example, to delete the first 4 bytes of program Alpha: `sub(4,0,"EAlpha`.
| augment( | This is used to insert data into a var. The syntax is: `augment(#ofbytes,Offset,"VarName`. For example, to insert 4 bytes at the beginning of appvar `Hello`: `augment(4,0,"UHello`.
Display the first 4 bytes of prgmPROG using `IS>(`
```
:Get("EPROG→Z
:Text('0,0,IS>(Z,16
:Text('°IS>(Z,16
:Text('°IS>(Z,16
:Text('°IS>(Z,16
```
## Data Structures, continued
Now let's make an array! First you need to know what you want. Do you want to
have 2-byte pieces of data or 1-byte? I like using one byte, so here is what we do:
```
:.0:Return
:Send(256,"VDat→Z ;We create a TempProg with 256 bytes of data called Dat.
:Z[rand,rand,rand ;write 3 random bytes.
:ClrDraw
:For(3
:Text('°Is<(Z ;Display the value at byte Z. Also increments Z.
:Text('°",
:DispGraph
:End
:Stop
```
That didn't really need a 256-byte variable, but I figured I would show how to
make one. Anyways, what that did was make a 256-byte tempprog (which the OS
automatically deletes once control is returned to the OS and you are on the
homescreen). Then, we stored 3 random values to the first three bytes, then we
displayed those values with commas after each number. If you want to use that 256
bytes for a matrix, instead, you can make it a 16x16 matrix and access elements
using a formula. For example, to read (Y,X): `(Z+X+Y*16`. That means that the data is stored in rows. That is why we take the row number and
multiply by 16 (that is the number of elements per row). This happens to be the
syntax that tilemaps are stored (stored in rows).
## Modes
| Name | Description |
|:------------ |:------- |
| Fix Text( | Use this to set the typewriter delay. The larger the number, the slower the typewriter text is displayed.
| Fix | See description below.
| Full | This is used to set 15MHz mode. Alternatively, if you add a number to the end `Full0` sets 6MHz, `Full1` sets 15MHz, `Full2` toggles the speed. 15MHz is only set if it is possible for the calc. This returns `0` if the previous speed setting was 6MHz, `1` if it was 15MHz.
| Output( | See description below.
### Fix
Use this to set certain modes. For all the modes that you want to set, add
the corresponding values together. For example, to enable inverse text
and inverse pixels, use `Fix 1+2` or simply `Fix 3`
Here are the modes:
* 1-Inverse text
* 2-Inverse pixels. Now, on pixels mean white and off means black.
In assembly terms, it reads from the buffer, inverts the data
and sends it to the LCD.
* 4-Disable ON key. This will allow ON to be detected as a key, too
* 8-Hexadecimal Mode. (Numbers are read as hexadecimal)
* 16-PixelTestOOB. Returns 1 for out of bounds pixel tests
* 32 - Display numbers as signed values.
If you want to use bit logic to set or obtain specific bits or info
about the current modes, you can do things like this:
Stores the current mode value:
```
:Fix →A
```
Set the first three modes without changing the rest:
```
:Fix or 7
```
Toggles mode 4 (enable/disable [ON] key)
```
:Fix xor 4
```
### Output(
This is used to change the font. The syntax is:
* `Output(0` will change to the default 4x6 font.
* `Output(1` will change to the variable width font.
* `Output(2` will allow you to use the 4x6 font at pixel coordinates
* `Output(3,font` will let use Omnicalc or BatLib styled fonts.
* `Output(4` will use the OS small font.
* `Output(5` will use the OS large font.
* `Output(°#` will change the draw logic (except for the OS fonts).
* 0=Overwrite
* 1=AND
* 2=XOR
* 3=OR
* 4=DataSwap.......Does nothing
* 5=Erase
Adding another argument to the first three will allow you to choose
your own custom fontset. The argument simply points to the start of the
fontset.
The output is a pointer to the fontset (custom or standard set), which could be useful if you wanted to read or process the built-in set.
For `Output(3,` remember that Omnicalc's font data starts at an offset of
11. So if you have a font called `BOLD`, you would do:
`Output(3,11+Get("EBOLD`
### Mode Examples
Using the output of `Full`, we can test if a calculator allows 15MHz.
```
:Full ;Sets 15MHz if possible
:If Full ;Sets 15MHz if possible, but also returns 1 if the previous `Full` was able to set 15MHz
:Then
:Text(0,0,"15MHz possible!
:Else
:Text(0,0,"6MHz only :(
:End
```
# Charts
## Key Codes
You can use this as a guide to the key values output by getKey
example, Clear=15
![Key Codes](https://i.imgur.com/r3ylxgN.png)
Also, there are the diagonal directions:
```5=Down+Left
6=Down+Right
7=Up+Left
8=Up+Right
16=All directions mashed
```
# Examples
Here is code that changes X and Y based on key presses.
```
:X+getKey(3
:min(-getKey(2,95→X
:Y+getKey(1
:min(-getKey(4,63→YParticles
:.0:
:0→X→Y
:Repeat getKey(15
:R▶Pθ(
:If getKey(9
:P▶Rx(Y,X
:X+getKey(3
:min(-getKey(2,95→X
:Y+getKey(1
:min(-getKey(4,63→Y
:End
:Stop
```
# Thanks
I have to give special thanks to Yeongjin Nam for their work on writing a
better tutorial for Grammer and as well Louis Becquey (persalteas) for their work on
writing a french readme/tutorial. Both of them have also made many valuable
suggestions that have helped make Grammer what it is right now. Thanks much!
Thanks to @NonstickAtom785 for the suggestions and (many) bug reports that I would not have found otherwise!
I also thank Hans Burch for reconstructing Grammer 2 after I lost my work. It must have been a tremendous amount of effort and tedium, and I greatly appreciate it. They've continued to provide valuable feedback about bugs and it has been extremely helpful.
Finally, I would like to thank the sites that have let me get the word out about
this project, especially [Omnimaga](https://www.omnimaga.org/index.php) and [Cemetech](https://www.cemetech.net/).