426 lines
24 KiB

# slope
slope is an interpreted s-expression based programming language. The interpreter is implemented in the [go programming language]( Those familiar with scheme or lisp will likely find themselves on reasonably familiar ground. Given its roots in golang, it wraps a lot of the go standard library in convenient ways for interpreted/script based programming. The [REPL]( comes with command completion, readline-style line editing, and procedure help/definitions (via the `usage` procedure).
**Table of Contents**
1. [Why another programming language?](#why-another-programming-language)
2. [Acknowledgements](#acknowledgements)
3. [The Language](#the-language)
1. [Types](#types)
2. [Special Forms](#special-forms)
3. [Library](#library)
4. [Preload](#preload)
5. [Modules](#modules)
1. [Package Management](#package-management)
2. [How Modules Are Processed](#how-modules-are-processed)
3. [Module Documentation](#module-documentation)
4. [Module Files](#module-files)
1. [main.slo](#main-slo)
2. [Additional Source Files](#additional-source-files)
3. [module.json](#module-json)
6. [Building From Source](#building-from-source)
7. [Running](#running)
1. [The REPL](#the-repl)
2. [One Liners](#one-liners)
3. [Slope Files](#slope-files)
4. [Other Options](#other-options)
8. [License](#license)
## Why another programming language?
Like many many projects on the internet: to learn and have fun. I have definitely learned different things than my [previous language project]( while working on this codebase, and I think have ended up with a much more realistically usable language (though your mileage may vary).
## Acknowledgements
This interpreter is based on some [existing code]( I rewrote sections of the lexer and parser (areas I enjoy working in), left the apply and environment setup alone, and made a few small changes to the eval function. As such, this could be considered a fork of that code with some added core language constructs as well as a significantly expanded library (another area I enjoy working in).
## The language
### Types
slope recognizes a few types, that are generally created and used dynamically:
- `number`
- implemented as `float64` under the hood
- hexidecimal can be written with a leading `0x`, as in `0xFFF`
- octal can be written with a leading `0`, as in `077`
- in strings, values can be escaped to characters as decimal, octal, or hex: `"\27[1mI am bold text\033[0m, I am not bold. I am a hex based char: \0x84"`
- `symbol`
- strings that represent language objects/constructs/vars
- can be created using `(quote my-symbol)` or `'my-symbol `
- `bool`
- as `#t` and `#f`
- anything can be cast to bool with a `#t` value, except `#f` (the only truly falsy value without using `~bool`, see below)
- `string`
- anything in "quotes" is a standard string literal
- allows for backslash escapes within the quotes (for chars by number or shortcuts to tab, newline, etc)
- anything in \`backticks\` is a raw string literal, no escapes occur (except backtick itself) and strings can span multiple lines
- `list`/`pair`
- includes the empty list (null)
- lists are the primary data structure
- `exception`
- an error response
- can be created using `(! "My exception text")`
- can be tested for with `(exception? some-var)`
- two functions exist to set behavior for exceptions:
- `(exception-mode-panic)`, will panic and halt execution when an exception is encountered (_default_)
- `(exception-mode-pass)`, will return the exception like any other value and it can be handled
- the two modes can be switched on and off at various points in the code
- `io-handle`
- represents files (in read or write mode), network connections, and string buffers
- passed by _reference_ rather than by value (unlike all other slope types)
If the _optional_ gui module is installed, the following become available:
- `gui-root`
- The application root of a gui
- Holds all windows and is used as the reference point for many gui based procedures
- Passed by _value_ like most slope types
- `gui-container`
- an organizational structure used for laying out widgets
- does not specify the type of container, just that the value is a `gui-container`
- `gui-widget`
- the interface building block of gui applications
- does not specify the type of widget, just that the value is a `gui-widget`
### Special Forms
There are a number of special forms in the language that will allow, for example, un-initialized symbols to be passed (for example, in `define`), among other things less available from regular lambdas (which are themselves a special form).
- `and`
- `apply`
- Applies a list of arguments to a given procedure
- `begin`
- `begin0`
- Like begin, but returns the value resulting from the evaluation of the first expression. No shorthand exists for `begin0`
- `cond`
- `case`
- `define`
- Lexically scoped, such that its use from within a lambda scopes any defined variables to the scope of that lambda and its child scopes
- `eval`
- Executes representations of slope code
- `exists?`
- Checks if the current environment is aware of the given symbol
- `filter`
- Takes a procedure that, in turn, takes a single argument. Each value from the second argument, a list, will be passed to the procedure. Anything truthy results are returned in a new list
- `for`
- A flexible, though verbose/complex, generalized looping construct that can be used to build other looping construct by way of macros or lambdas
- `if`
- `lambda`
- The body of a lambda has an implied `(begin ...)` surrounding it. As such, you may place non-nested procedure calls or values inside of a lambda body and the last will be returned
- Variadic lambdas can be created by using the special param name `args-list` or `...` (which will be a list of all of the remaining arguments when accessed rom the lambda body). This param should be the last param in the lambda definition, as it will eat all arguments that come after its position. Any params defined after `args-list`/`...` have undefined behavior (they will generally not be created)
- A paramater symbol can include an `@` followed by the name of a procedure. For example: `(lambda (input@number?) (+ input input))`. If tha is done, the argument passed to the lambda will first be passed to the given procedure. This allows for a form of runtime type checking. Generally the procedure given should be a predicate, but any procedure will work (the return value will be coerced to a boolean value). This creates a flexible type checking system. To create a new "type" simply create a predicate that is capable of recognizing that type, then use the `@` paramater syntax to enforce it. `@` paramaters _do_ work with `...`: they will pass the full list to the predicate, and will _not_ run the predicate on each list item.
- `load`
- Loads and executes a slope file. Absolute path to file must be used
- `load-mod`
- Loads a module. Module _name_ should be used
- `load-mod-file`
- Used within modules to load additional files
- `macro`
- Similar to `lambda`, macro is variadic using the same argument names. It also has an implied `begin`.
- All values passed to `macro` will be unevaluated, which is the main difference between `macro` and `lambda`. This allows you to manipulate code that is given, rather than just values.
- `or`
- `quote`
- Can be shorthanded so that `'(1 2 3)` is equal to `(quote (1 2 3))`
- `set!`
- `usage`
- Used to display the procedure signature and any additional information about the procedure, mostly used interactively
While not a special form from a compiler implementation standpoint `list` has some syntactic sugar that is worthy of note. These four lines will all produce the same list:
``` slope
(list 1 2 3)
[1 2 3]
(quote 1 2 3)
'(1 2 3)
Quote and list both have some syntactic sugar to create a shorthand for their usage (`'()` for quote and `[ ... ]` for lists).
<summary>Special Forms API Description</summary>
<p>Anything inside `[]` is a placeholder representing an argument. Anything inside `[[]]` is a placeholder representing an _optional_ argument. `...` indicates a variadic value.</p>
<p>What the procedure returns is indicated, though any procedure can return an exception in addition to the given type.</p>
<li><code>(quote [expression...])</code>: <code>bool</code></li>
<li><code>(if [condition check: expression] [true branch: expression] [[false branch: expression]])</code>: <code>value</code></li>
<li><code>(and [expression...])</code>: <code>bool</code></li>
<li><code>(or [expression...])</code>: <code>bool</code></li>
<li><code>(cond [([condition check: expression] [expression])...])</code>: <code>value</code></li>
<li><code>(case [value] [([match-value: expression] [expression])...])</code>: <code>value</code></li>
<li><code>(for [([symbol] [init: value] [update: expression])...] [([test: expression] [return-value: expression])] [body: expression...])</code>: <code>return-value (evaluated)</code>
<li><code>(set! [variable name: symbol] [value|expression])</code>: <code>value/expression</code></li>
<li><code>(define [variable name: symbol] [value|expression|procedure])</code>: <code>value/expression</code></li>
<li><code>(lambda [([[argument: symbol...]])] [[expression...]])</code>: <code>procedure</code></li>
<li><code>(macro [([[argument: symbol...]])] [[expression...]])</code>: <code>procedure</code></li>
<li><code>(begin [expression...])</code>: <code>value</code></li>
<li><code>(begin0 [expression...])</code>: <code>value</code></li>
<li><code>(filter [test: procedure] [list])</code>: <code>list</code></li>
<li><code>(load [filepath: string...])</code>: <code>symbol</code></li>
<li><code>(load-mod [filepath: string...])</code>: <code>symbol</code></li>
<li><code>(exists? [symbol...])</code>: <code>bool</code></li>
<li><code>(load-mod-file [filepath: string...])</code>: <code>symbol</code></li>
<li><code>(usage [[module|builtin-procedure: string]] [[module-procedure: string|#f]])</code>: <code>()</code> Will display usage information for the given procedure or runtime value. If no argument is given <code>usage</code> will list known procedures. If a module name and `#f` are passed a list of known procedures for that modules will be printed. If a module name and a known procedure for that module are given then information about the procedure will be printed.</li>
<li><code>(eval [expression|value] [[evaluate-string-as-code: bool]])</code>: <code>value</code></li>
<li><code>(coeval [expression...])</code>: <code>()</code></li>
### Library
The slope library documentation has been moved out of the readme. Suggested resources are:
- Using `usage` at the repl. Try: `(usage usage)`, for info on how to use usage
- [slope language documentation](
- [slope procedure reference](
#### GUI
Slope offers a limited wrapper around the [fyne]( gui toolkit. This is a fully _optional_ build item. See the [build instructions](#building-from-source) for information about building, or not, the gui module.
The module does not offer any free/object drawing or animation. It offers a selection of widgets and containers and some ability to manipulate them and use callbacks from them. This allows for many practical applications that are based more around form fields than graphics.
See the procedure reference mentioned above for more information.
To get a taste of how things look, here is a screen shot of some widgets in a window created from slope:
![Demonstration of slope gui](/images/slope-gui-demo.png)
\*the general write, read, and close operations in the IO section apply to net objects as well as files and string buffers
## Preload
slope allows for files in a specific directory to be automatically loaded. For this to occur slope should be invoked with the `-L` flag. When that flag is passed slope will do its normal setup routine then preload all files in the preload folder then go on to whatever its main task is (repl, run file, run single line from command).
slope will use the variable `$SLOPE_PRELOAD_DIR` if it is defined and not empty. Otherwise, if `$XDG_DATA_HOME` is defined and not empty it will use `$XDG_DATA_HOME/slope/preload/`. Lastly it will use `~/.local/share/slope/preload/`.
Unlike modules, arbitrary code execution _can_ occur with preloads (not just `define` expressions). The rationalle is that you, the user, have put this code there and as such it is considered safe and in your control. Preloads are a really easy way to make procedures and variables outside of the standard library, but that you regularly use, available at a repl session without having to manually load them.
## Modules
slope has a basic module system. When you use the `load` procedure you are loading a single file from a given path. Anything in that file will be run when it is loaded. Using `load-mod` allows you to load a module from a designated location on your system by simply passing the module's name (as represented by the directory name it is contained within).
`load-mod` will load modules from the first non-empty-string value it finds out of these options:
2. `$XDG_DATA_HOME/slope/modules`
3. `~/.local/share/slope/modules`
4. `/usr/local/lib/slope/modules`
So, a hypethetical module named `test` would be found at `~/.local/share/slope/modules/test`.
Note that the fourth item, above, is the global (system-wide) module directory. `slp` can install modules to this directory by passing the `-g` or `--global` flag while acting as a user that has access to writing files in the `/usr/local` heirarchy. You may, alternately, clone modules directly to this path. A local module will always be used before a global module.
### Package Management
slope has a package manager, `slp`, available at []( Once installed you will be able to search/browse, install, remove, and update modules. You can also use `slp` to generate new module skeletons, read docs, etc. The `slp` repository has information on how to get modules added to the package registry. This is, at present, the best way to deal with modules and is highly recommended but by no means required. The same thing can be done by finding a repository with a module and cloning it to your module path. So long as it is a valid module it will be loadable with `load-mod`.
Additionally, the slope interpreter has an `-install` flag that will copy a _local_ module directory (say, one cloned via git) to the proper place for it to be loadable from slope. So a simple: `git clone [...] && slope -install .` should do the job for simple situations. A module installed this way can still be removed by `slp`'s remove command and will be listed among the installed modules by `slp`.
### How Modules are Processed
When `load-mod` is called the module will be located and `main.slo` will be parsed. All expressions are ignored except `define`, `load-mod`, and `load-mod-file`. Arbitrary code cannot be run (there are no side effects to `load-mod` or `load-mod-file` other than populating the global environment with the defined values from the given module).
A `define` expression _will still be fully evaluated_. As such, something like: `(define twenty (+ 15 5))` is perfectly fine. But something like `(display "I am in a module")` will not be evaluated.
Calling `load-mod` from within a module allows modules to have their own dependencies. Calling `load-mod-file` allows modules to be made up of more files than just `main.slo`. More info about files can be found in the next section.
The interpreter, as of version `1.0.0`, uses namespaces to sandbox modules and reduce the possibility of symbol name collisions. Modules can be aliased to a symbol different than the module name via `load-mod`. To reference a symbol from a module use the following form: `[module-name|alias]::[symbol]`. For example:
``` scheme
(load-mod hash-table)
(define h (hash-table::make-hash-table))
or with an alias:
``` scheme
(load-mod hash-table ht)
(define h (ht::make-hash-table))
### Module Documentation
Modules may (one could argue _should_) contain a value named `_USAGE` in their `main.slo` file. This value should be an association list detailing each procedure. It should take the form of:
``` scheme
; This is for a module named: "mymodule"
(define _USAGE [
["my-proc" "(my-proc [name: string]) => bool\n\nFurther info can go here. Provide what would be useful"]
["my-other-proc" "(my-other-proc [name: string] [count: number]) => string"]
This value is special and will be parsed so that the information can be retrieved at the repl via the `usage` command.
``` scheme
(load-mod hash-table) ; load a module for this example
(usage) ; will list the builtin symbols and list the loaded modules
(usage +) ; will list info about the `+` procedure, a builtin
(usage hash-table::) ; Will list the symbols with usage
; information in the hash-table module
(usage hash-table::make-hash-table) ; will list info about 'make-hash-table'
It is good form to always include the procedure signature as the first item in the usage instructions. Arguments are included in the format: `[name: type]`. Optional arguments use double square brackets: `[[name: type]]`. The output should be denoted after the close of the function signature via `=> type`. If multiple types can be used or returned use a `|`: `(my-func [input: number|string]) => procedure|#f`. If a specific value can be returned, you can include that, as `#f` was used in the previous example. Many procedures return some value _or_ false (`#f`) to indicate a failure.
### Module Files
#### main.slo
Taking our above example of a module named `test`, the bare minimum that the folder `~/.local/share/slope/modules/test` should contain is a file named `main.slo`. When a module is loaded, this is the file that is looked for. If it is not present, loading will fail and a runtime panic will result.
#### Additional source files
You may, optionally, have other code files present in the module and can load them by calling `load-mod-file`. This procedure should be given a path relative to the module root directory. So within our `main.slo` file we might call something like: `(load-mod-file "./submodules/unit-tests.slo")`. Something to note is that a call to `load-mod-file` cannot jump out of the module's root directory (doing something like `../../../` or `/etc/something` will result in a runtime panic).
#### module.json
Modules found in the package registry will include this file. It contains metadata related to the package including things like description, version, repository url, homepage, author, and, most importantly the dependency list (so that a modules dependencies can also be installed).
If you are making a module and do not know what should be here you can look at an existing module to get a sense of it. Or, with `slp` installed, you can just run `slp gen` and it will build out your module skeleton for you.
## Building from source
A Makefile is available. Running `make` will build in the local directory. By default, this will _not_ include the gui module. To include any of the optional modules, create a variable `$SLOPE_TAGS` that includes the tags/features (`clipboard`, `dialog`, `gui`, or the catchall: `all`) you want to add. So in bash, for example, `make SLOPE_TAGS="clipboard dialog"`. Running `sudo make install` after running `make` will install the software globally (sudo may or may not be needed depending on your setup, or the functionality may be provided by a different tool such as doas). _slope_ was written with Go 1.16, but will likely build on older versions with limited modifications.
``` sh
git clone
cd slope
make # or `make SLOPE_TAGS="tags..."`
# For global install run the following
# after running `make` or `make SLOPE_TAGS="tags..."`
sudo make install
If you prefer to build directly with the go compiler rather than the provided makefile you can do a basic build with one of two commands (depending on whether or not you want any optional modules):
``` sh
go build
# WITH clipboard and gui, for example
go build -tags "clipboard gui"
The above build methods, without the makefile, will not provide a version hash to the program, but will successfully build the program. The makefile has some nice features and is definitely the recommended route.
If you run into issues getting the gui version to build, it may be because of slope's dependency on the [fyne]( toolkit. Their issues or build instructions may be helpful. That said, gui is presently an experimental feature and has been made an optional component so that an upstream component will not block installs of the interpreter.
## Running
All of the shell commands in this section assume slope is on your `$PATH`.
### The REPL
To run the slope REPL environment execute the following at your terminal prompt:
``` sh
You can, at your option, use the `-L` flag to preload the environment with your [preload](#preload) content.
From the REPL you can execute `(license)` to view the license. To exit, you execute `(exit)`.
While in the REPL readline compatible key commands are available. For a list of compatible key commands please see [this list](
In addition to key commands, the repl supports command completion for any identifier that has `usage` information available. Since `usage` supports getting usage information from _loaded_ (via `load-mod`) modules, any usage information provided by said modules will be included in completion. To complete a known identifier press the `Tab` key. The first found completion will appear. Subsequent presses of the `Tab` key will cycle through any other completion candidates that were found.
### One Liners
To run a one liner execute the following at your terminal prompt:
``` sh
slope -run "(display (+ 1 3))"
You may, of course, replace the contents between the quotes with your own one liner. In general all features of slope are available from a one liner, including loading modules.
You may also leverage `-run` to create pipelines. By passing `--` to `-run` slope will read from `stdin` until `EOF`:
``` sh
cat myfile.slo myotherfile.slo | slope -run -- | less
You can, at your option, use the `-L` flag to preload the environment with your [preload](#preload) content.
### Slope Files
To run a file execute slope with the file path (an example path has been provided below):
``` sh
slope ./myfile.slo
### Other Options
To see command line options:
``` sh
slope -h
To load all slope file in your [preload](#preload) folder before proceeding to run the file, one liner, or repl:
``` sh
slope -L
To see version information:
``` sh
slope -v
To install a local module:
``` sh
slope -install /path/to/module
The filepath should lead to a folder that contains at the very least a `main.slo` file. If installing the module would overwrite an existing module on the module path you will be warned and need to give consent to overwrite the module.
To turn on interpreter runtime stack traces:
``` sh
slope -debug
## License
slope is available under the terms of the MIT license. See either the `LICENSE` file in this repository or run `(license)` from slope (via file, `-run`, or REPL).