Using the Compiler
zrpc is written in Rust, and uses the Cargo build system. To learn more about Cargo, see https://doc.rust-lang.org/cargo/.
The compiler can be downloaded at https://codeberg.org/zbwells/zrpc.
If you don't wish to install the compiler anywhere, do something like this:
cargo run <program name>.rpc
with the program maybe being something from the sample programs directory (/prog in the Git repo). The compiler will then write the executable to "zrpc_out", or if you want, you can specify another name for the output by providing two arguments on the command-line:
cargo run <program name> <output name>
There is currently no robust command line arguments system in place, nor any interesting options.
The command-line usage is likely subject to change, as it's the place where the least work has been done.
The Stack
zrpc is a "stack-oriented" language, meaning that all functions of the language implicitly involve the usage a stack data structure. Here's an example:
4 5 +
When the zrpc compiler parses this snippet, it will read the reverse-polish math from left-to-right, first seeing 4, then 5, and pushing them both onto the stack. When it reaches +, it will pop 4 and 5 off the stack, add them together, and push the result of the operation back onto the stack. Here's an example with an extra component:
4 5 + 10 *
In this case, 4 5 + resolves to 9, making the snippet into this:
9 10 *
which evaluates to 90.
The number of objects that a function (or operator; they are interchangeable in zrpc) pops off the stack is called its "arity". The arity of a function is usually the same as the number of arguments the function accepts.
Each function also has an arbitrary number assigned to it which determines how many objects it places back onto the stack after it returns, which is called its "pointer increment" value. In the reference pages for each built-in function, these will be referred to as a function's "incptr" value.
zrpc calculates the position of each object on the stack at compile time, aside from objects added to the stack via user input, which are handled at runtime. This results in some strange behavior regarding logic statements:
5 5 ?{
6 4 3 + =
}
In this conditional statement, both instances of 5 will be popped off the stack by ?{, and 4 3 + will resolve to 7 and be printed and popped off the stack by =, but 6 will remain on the stack after the conditional finishes— however, if instead the statement was:
4 5 ?{
6 4 3 + =
}
The conditional would not execute, but the compiler would still compute stack positions as if the conditional had executed— it can't tell which will happen, and with the current implementation, conditionals do not have their own local variable scope / local stack, in contrast to user-defined functions, which do. This results in the stack pointer incrementing and decrementing in the same manner regardless of conditional execution, meaning that a 0 will be added to the stack instead of a 6 in this case. For this reason, it is best practice to pop any unwanted variables off the stack prior to terminating a conditional, like so:
x 5 ?{
6 4 3 + = pop
}
which prevents the ghost 0 from being pushed onto the stack, as the stack pointer is manually decremented at the end of the conditional, with the stack pointer neither being increased nor decreased as a result of the conditional body.
This behavior may be changed in a future version, as it is somewhat difficult to understand, despite being technically more flexible and easier to implement than individual stack scope for conditionals.
Syntax
The syntax for zrpc is all reverse-polish, except for places where it's not. The preprocessor and the user-defined function parsing components are not reverse polish.
To get a better look at the syntax, take a look at a sample program, piece by piece— in this case, progs/hellotimes.rpc from the git repo.
This program asks for a number, and then prints out "Hello World!" the amount of times specified by the number.
;; Hello World! however many times
(hellotimes num)
num 0 ?{
q
}
"Hello World! " printstr pop
num 1 - hellotimes <-
:
5 hellotimes nl
The program starts out with a comment, which is delimited by a pair of semicolons:
;; Hello World! however many times
And then immediately defines a function:
(hellotimes num)
This function is named "hellotimes", and takes one argument, called num.
num 0 ?{
q
}
The program then uses a conditional (equivalent to "if num == 0 { exit() }") to check to see if the number is equal to zero, and if it is, it exits the program.
"Hello World! " printstr pop
Otherwise, it prints Hello World! , and then pops the string off the stack manually to get it out of the way—
num 1 - hellotimes <-
and then to complete the recursive loop, subtracts 1 from num, and then passes it to hellotimes— this will continue until the terminal condition is met (num being 0), and the function terminates. The <- signifies a function return.
:
All functions and macros must be terminated with a colon.
Any operations outside of a function or macro must come after any defined functions or macros.
5 hellotimes nl
The program starts off by calling hellotimes with a 5, meaning that Hello World! should be printed 5 times, and after hellotimes finishes executing, the program ends by printing a newline.
A more comprehensive understanding of the syntax can be gained by reading more sample programs. A better guide might also be written at some point.
Math
Math in zrpc is all postfix, and somewhat resembles the Unix arbitrary precision calculator dc.
Here is an example:
5 4 + 10 * 2 -
5 and 4 will be put on the stack, and then added—
9 10 * 2 -
and then 10 will be put on the stack, and 9 and 10 will be multiplied—
90 2 -
and finally, 90 and 2 will be popped off the stack and subtracted, and the result will be:
88
This is the gist of how all math executes in zrpc. The kicker is that all other functions also operate in this way, as all functions accept function arguments in post-fix syntax, including user-defined ones.
Logic
Logic works via logical built-in functions, each of which accept two parameters— specifically
x y ?{x y !?{x y <?{x y >?{
which correspond to
if (x == y)if (x != y)if (x < y)if (x > y)
respectively.
Each of these logical functions only accept numeric data as parameters— passing strings or characters will result in undefined behavior, although in some cases it may appear to work properly.
In order to use these with strings or characters, use streq and cheq. These functions check equality between strings or between characters. See Strings and Text Processing for more information on streq and cheq.
Here is a sample program containing conditionals:
; Program to print the fibbonacci sequence to the nth term.
#main "How many fibbonacci numbers should be printed? \n=> "
printstr getnum prnfibs :
(fib n)
n 3 <?{
1 <-
}
n 1 - fib n 2 - fib + <-
:
(prnfibs n)
n 0 >?{
n fib n 1 - prnfibs pop = nl
}
<-
:
@main
The above program can be found at the git repo: progs/nfibs.rpc.
Functions
Function definitions in zrpc are done similarly to how they're done in Lisp-languages, but instead of something like (define (fact n) <function body>), you would instead just do (fact n) <function body> :.
Functions in zrpc can have any number of arguments, and to put variables on the stack from the functions, write the name of the parameter inside the function body, and the item with that name will be placed on the stack.
Parameters are all Pass-by-value, excluding strings, where a pointer to the string is passed instead— the language does not yet support passing dynamically allocated strings back and forth between functions, although it is very much possible— it just creates untrapped memory leaks.
See the Syntax page for a full example of a simple function.
Macros
Macros in zrpc are equivalent to C #define preprocessor directives.
Here is an example of how one might be used:
#limit 128
@limit 127 >?{
"127 is within the limit" q
}
Wherever @ and the name of the macro is, that text will be replaced with the text defined by the macro. This is idiomatically used to place toplevel statements above functions, which is normally not possible. Most of the sample programs provided in /progs on the git repo use this form.
#main emptybuf listhandle :
(listhandle list)
list strlen 1 >?{
"List: " printstr pop
list printstr nl
}
"Enter a number to add to the list (ctrl+c to quit): " printstr pop
list getstr append " " append
listhandle
:
@main
Recursion
Recursion operates in zrpc much in the way it does in other programming languages. Each user-defined function in zrpc generates an equivalent function in C, and so the properties of recursion more or less mimic the properties of recursion in C. (i.e. there is no tail-call optimization.)
The typical sample program for recursion tends to be a factorial function, which is available at progs/fact.rpc.
Here is the text:
;; Calculates factorials of numbers
#main
getarg "\0" streq 1 ?{
q
}
atof fact = nl
:
(fact n)
n 1 ?{
n <-
}
n 1 - fact * <-
:
@main
This particular version grabs a number from the command-line arguments, and computes the factorial of that number, rather than asking for a number once the program starts.
Strings
Strings in zrpc are handled different depending on whether they are string-literals: something like "hello" or "goodbye", text surrounded by double-quotes written directly in the source code, or whether they are dynamically allocated char-arrays, as in the case of user-input strings such as generated through getstr or emptybuf.
There are many built-ins that can be used to manipulate strings, although some of them (append in particular) can only be used with dynamically allocated strings.
Here is a sample program (available at progs/list.rpc):
;; program to test string appending functionality
#main emptybuf listhandle :
(listhandle list)
list strlen 1 >?{
"List: " printstr pop
list printstr nl
}
"Enter a number to add to the list (ctrl+c to quit): " printstr pop
list getstr append " " append
listhandle
:
@main
Math Operators
In zrpc, mathematical operators are functions, and so they have the same properties as normal built-in functions do.
+
- arity: 2
- incptr: 1
Addition.
5 4 +
will resolve to 9.
-
Subtraction.
5 4 -
will resolve to 1.
*
Multiplication.
5 4 *
will resolve to 20.
/
Division.
20 5 /
will resolve to 4.
^
Exponentiation.
5 2 ^
will resolve to 25.
%
Modulus.
5 4 %
will resolve to 1.
=
Print number and pop number off of stack. Useful for getting those pesky numbers out of your way after calculations are done.
5 2 ^ 10 * 50 + =
Will leave no remaining items on the stack, but will print 300.
Strings and Text Processing
This section contains reference for String-related built-in functions.
- append
- ascii
- atoc
- atof
- cheq
- emptybuf
- fprintstr
- getch
- getstr
- printchr
- printstr
- ref
- streq
- strlen
- strword
append
- arity: 2
- incptr: 1
Concatenates two strings together, although the first one must be a dynamically allocated buffer, either via emptybuf, or via getstr.
Example:
emptybuf "garbage" append
ascii
- arity: 1
- incptr: 1
Pops number off the stack, and if possible, converts it to its ascii value. Undefined behavior for floating point numbers.
Usage:
65 ascii
is equivalent to A.
atoc
- arity: 1
- incptr: 1
Converts a character to its ascii integer value.
Usage:
"ABC" 0 ref atoc =
will print out 65.
atof
- arity: 1
- incptr: 1
Takes a string and converts its characters to a single decimal number, if possible.
Usage:
"12.5" atof
is equivalent to 12.5.
cheq
- arity: 1
- incptr: 1
Checks two characters and outputs 0 if they are different, and 1 if they are the same. Useful in conditional statements.
Accepts two arguments, despite what the arity would suggest. The first argument remains on the stack, and so the arity is still 1. Behavior subject to change depending on feedback.
Usage:
'a 'b' cheq =
Should print 0, because the two characters are different.
emptybuf
- arity: 0
- incptr: 1
Creates an empty dynamically allocated string buffer on the stack, of size 2048. More buffers need to be allocated if more than 2048 bytes are required for the target.
Example:
emptybuf "this" append "is" append "sparta" append
fprintstr
- arity: 1
- incptr: 1
Print a string to a specific file. Takes two arguments, the first being the file, and the second being the string to print to the file. The arity is still 1, because the file does not get removed from the stack. Behavior subject to change.
Usage:
"file.txt" wfopen "some words\n" fprintstr
getch
- arity: 0
- incptr: 1
Gets a character from stdin. (command-line input)
Usage:
getch printchr recurse
will infinitely get characters from the CLI and print them out.
getstr
- arity: 0
- incptr: 1
Dynamically allocates space for a string, and then fetches a newline-terminated string from stdin.
Usage:
getstr printstr pop
- arity: 1
- incptr: 1
Prints a number, but doesn't pop the number off the stack.
Usage:
5 print
printchr
- arity: 1
- incptr: 1
Prints out a character.
'a' printchr
printstr
- arity: 1
- incptr: 1
Prints out a string.
Usage:
"this string will be printed" printstr
ref
- arity: 1
- incptr: 1
Accepts two arguments, despite having an arity of 1. Fetches whatever character is at the index provided in a string. The first argument is the string, the second argument is the position to look for. The position argument is popped off the stack, but the string is not.
Usage:
"garbage" 2 ref printchr
will print r.
printchr
- arity: 1
- incptr: 1
Prints out a character.
'a' printchr
strlen
- arity: 0
- incptr: 1
Gets the length of a string.
Usage:
"garbage" strlen =
will print 7.
strword
Fetches a word from a string. A word is any ascii text separated by spaces. In the string "I eat food", strword would first return "I", then "eat", and then "food", each on subsequent calls.
Usage:
"I eat food" strword pop strword printstr
Will print eat.
TODO: Make this page better.
Files, Input, and CLI Arguments
This section provides a reference for various I/O related built-in functions.
afopen
- arity: 1
- incptr: 1
Pops string off the stack, and passes string as location for filename to open. Opens file in append mode (file pointer starts at end of file, writing enabled).
Example:
"Add number to 'file.txt': " printstr pop
"file.txt" afopen getstr fprintstr
Will append a number typed into stdin into "file.txt".
argc
- arity: 0
- incptr: 1
Fetches the number of arguments provided at the command line.
eof
- arity: 0
- incptr: 1
End-of-file object. Equivalent to -1, or EOF in C.
Example:
(getstdin)
getch d eof cheq 1 ?{
q
}
printchr getstdin
:
This function echoes whatever data is input to stdin until an EOF is found, and then it terminates.
fgetch
- arity: 0
- incptr: 1
Fetches a character from a file handler object.
Usage:
"file.txt" rfopen fgetch printchr
will print the first character from "file.txt".
flushinp
- arity: 0
- incptr: 1
Removes any lingering characters from stdin before next usage of stdin is needed. Legacy function— currently useless, as all functions that use stdin clear stdin before returning.
getarg
- arity: 0
- incptr: 1
Fetches an argument from the command-line. Fetches the next argument sequentially with each usage.
Example:
getarg printstr streq "\0" 1 ?{ recurse }
will print out every argument on the CLI until it cannot find any more.
getch
- arity: 0
- incptr: 1
Gets a character from stdin. (command-line input)
Usage:
getch printchr recurse
will infinitely get characters from the CLI and print them out.
getnum
- arity: 0
- incptr: 1
Gets a number from stdin, specifically puts it in numeric form. Undefined behavior if a string is entered.
Usage:
getnum 5 + =
getstr
- arity: 0
- incptr: 1
Dynamically allocates space for a string, and then fetches a newline-terminated string from stdin.
Usage:
getstr printstr pop
head
- arity: 0
- incptr: 1
Will get the first character from a string.
Example:
"garbage" head printchr
will print g.
rfopen
- arity: 1
- incptr: 1
Opens a file in read mode.
Usage:
"file.txt" rfopen fgetch printchr
will print the first character from "file.txt".
tail
- arity: 0
- incptr: 1
Copies a string, but removes the first character.
Usage:
"garbage" tail printstr
will print arbage.
wfopen
- arity: 1
- incptr: 1
Opens a file in write mode.
Usage:
"file.txt" wfopen "hello" fprintstr
will put hello in file.txt.
Logical Operators
- arity: 2
- incptr: 0
In zrpc, logical operators are functions, and so they have the same properties as normal built-in functions. Each logical operator has an arity of 2 and an incptr of 0.
?{
Equivalent to "if (num1 == num2) {" in C, where both objects popped off the stack are numeric.
Usage:
1 1 ?{
"equal numbers" printstr q
}
All logical operators work the same way, and there are no "and" or "or" operators.
!?{
Equivalent to "if (num1 != num2) {" in C.
Usage:
1 2 !?{
"unequal numbers" printstr q
}
<?{
Usage:
1 2 <?{
"1 is less than 2" printstr q
}
>?{
Usage:
2 1 >?{
"2 is greater than 1" printstr q
}
Miscellaneous Stack-related Functions
This section provides a reference for varios stack-manipulation related built-in functions.
=
- arity: 1
- incptr: 1
Prints out a number, and pops the number off the stack.
Usage:
<number> =
Example:
5 =
c
- arity: n/a
- incptr: n/a
Sets the stack pointer back to 0, effectively clearing the stack.
Usage:
<program> c
d
- arity: 0
- incptr: 1
Duplicates whichever object is currently on top of stack.
Usage:
<object to duplicate> d
Example:
5 4 d
would resolve to
5 4 4
pop
- arity: 1
- incptr: 0
Decrements the stack pointer by one, effectively popping an object off the stack.
Example:
"words" printstr pop
In the above case, printstr would print "words", and then pop would remove the string from the stack.
q
- arity: n/a
- incptr: n/a
Exits the program, equivalent to exit(int) in C.
Usage:
q
recurse
- arity: n/a
- incptr: n/a
Calls the top-level program present in the source, allowing it to call itself despite having no name.
Example:
"yes" printstr recurse
Will infinitely print "yes".
swap
- arity: 0
- incptr: 0
Swaps the two most recent positions on the stack with each other.
Example:
4 5 swap
will result in:
5 4
unpop
- arity: n/a
- incptr: n/a
Increments the stack pointer by 1, without modifying the value of the object on the stack, effectively bringing back any data that was previously popped off, as long as nothing else had been pushed back in its place at any point. Very situational.
Example:
5 4 * unpop *
will result in 80.