truthy
falsy
falsy
truthy
truthy
truthy
Bass is an interpreted, functional scripting language riffing on ideas from Kernel and Clojure, written in Go.
The following is a list of all the weird terms involved in the language. There's some real nerdy stuff, like operative (wow), and some milquetoast concepts given fun names like thunk (whee).
In the end it's a pretty tiny functional language with some interesting twists that allow it to do a lot with a little.
(wrap <builtin: (symbol? val)>)
:symbol?
symbol?
Symbols cannot be parsed from a thunk response, so they are sometimes used as sentinel values to indicate the end of a response stream.
(def nums
(list->source [1 2]))
[(next nums :end)
(next nums :end)
(next nums :end)]
(
1
2
end
)
(def foo {:a {:b 42}})
foo
foo:a:b
42
Symbols are also functions which fetch their binding from a scope, with an optional default value.
(def foo 123)
[(:b {:a 1 :b 2 :c 3})
(:foo (current-scope))
(:b {:a 1} 42)
]
(
2
123
42
)
A symbol prefixed with a :
is called a keyword. It is used to construct the symbol itself rather than fetch its binding.
:im-a-symbol!
im-a-symbol!
[:+ 1 2 3]
(
+
1
2
3
)
_
(underscore) is a special constant value used to discard a value when binding values in a scope.
(def (a & _) [1 2 3])
a ; the only binding in the current scope
1
_
is also used when null
is just not enough to express how absent a value is - for example, to record overarching commentary within a module.
; Hey ma, I'm a technical writer!
_
_
A set of symbols bound to values, with a list of parent scopes to query (depth-first) when a local binding is not found.
All code evaluates in a current scope, which is passed to operatives when they are called.
{
:local-binding |
"hello" |
||||||||||||||||||
{
} |
}
A combiner which receives its operands unevaluated, along with the scope of the caller, which may be used to evaluate them with eval
.
Operatives are defined with the (defop)
operative or constructed with (op)
.
(defop quote-with-scope args scope
[args scope])
(quote-with-scope a b c)
(
(
a
b
c
)
{
:*memos* |
<host: .>/bass.lock |
||||||||||||
:quote-with-scope |
(op args scope [args scope]) |
||||||||||||
{
} |
}
)
A stream of values which may be read with (next)
.
All scripts can read values from the *stdin*
source, which reads JSON encoded values from stdin
.
A source may be constructed from a list of values by calling list->source
, but they are most commonly returned by run
.
(def nums
(list->source [1 2 3]))
[(next nums)
(next nums)
(next nums)]
(
1
2
3
)
A location of a file or directory within a filesystem.
Bass distinguishes between file and directory paths by requiring a trailing slash (/
) for directories.
Directory paths can be extended to form longer paths:
dir/sub/file
./some-dir/sub/file
The above syntax is called path notation. Path notation is technically just reader sugar for nested pairs:
((dir ./sub/) ./file)
./some-dir/sub/file
A name of a command to be resolved to an actual path to an executable at runtime, using $PATH
or some equivalent.
.cat
.cat
A file or directory path relative to a directory on the host machine, called the context dir.
The only way to obtain a host path is through *dir*
, which is set by Bass when loading a module.
*dir*
<host: /tmp/bass-scope2692694031>/
(-> ($ ls $*dir*)
(with-image (linux/alpine))
run)
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.02s]=> upload <host: /tmp/bass-scope2692694031>/ [0.01s]-> transferring /tmp/bass-scope2692694031: 2B [0.00s]=> copy / / [0.01s]=> ls <host: /tmp/bass-scope2692694031>/ [0.20s]
null
A serializable object representing a command to run in a controlled environment.
Throughout this documentation, thunks will be rendered as space invaders to make them easier to identify.
Thunks are run by the runtime with run
or read
. Files created by thunks can be referenced by thunk paths.
A thunk that doesn't specify an image will be interpreted as a native Bass thunk which can be use
d or load
ed in addition to being run
.
(.git (linux/alpine/git))
{
:args |
(
) |
||||||||||||||
:stdin |
(
) |
}
A controlled environment for a thunk's command to run in. A thunk's image determines the runtime that is used to run it.
Concretely, a thunk's image is either a scope specifying a reference to an OCI image, or a parent thunk to chain from.
To reference an image in a registry, use the linux
path root - which uses resolve
under the hood to resolve a repository name and tag to a precise digest.
(linux/alpine)
{
:image |
{
} |
}
To use an image with a thunk, use from
or with-image
:
[(from (linux/alpine)
($ echo "Hello, world!"))
(with-image
($ echo "Hello, world!")
(linux/alpine))
(-> ($ echo "Hello, world!")
(with-image (linux/alpine)))
]
(
{
:image |
{
} |
||||||||||||||
:args |
(
) |
}
{
:image |
{
} |
||||||||||||||
:args |
(
) |
}
{
:image |
{
} |
||||||||||||||
:args |
(
) |
}
)
To reference an OCI image archive created by a thunk, set :file
to a thunk path.
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.02s]=> apk add skopeo [1.54s]â–• fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gzâ–• fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gzâ–• (1/18) Installing containers-common (0.50.1-r0)â–• (2/18) Installing device-mapper-libs (2.03.17-r1)â–• (3/18) Installing libgpg-error (1.46-r1)â–• (4/18) Installing libassuan (2.5.5-r1)â–• (5/18) Installing ncurses-terminfo-base (6.3_p20221119-r0)â–• (6/18) Installing ncurses-libs (6.3_p20221119-r0)â–• (7/18) Installing pinentry (1.2.1-r0)â–• Executing pinentry-1.2.1-r0.post-installâ–• (8/18) Installing libgcrypt (1.10.1-r0)â–• (9/18) Installing gnupg-gpgconf (2.2.40-r0)â–• (10/18) Installing libbz2 (1.0.8-r4)â–• (11/18) Installing sqlite-libs (3.40.1-r0)â–• (12/18) Installing gpg (2.2.40-r0)â–• (13/18) Installing npth (1.6-r2)â–• (14/18) Installing gpg-agent (2.2.40-r0)â–• (15/18) Installing libksba (1.6.3-r0)â–• (16/18) Installing gpgsm (2.2.40-r0)â–• (17/18) Installing gpgme (1.18.0-r0)â–• (18/18) Installing skopeo (1.10.0-r5)â–• Executing busybox-1.35.0-r29.triggerâ–• OK: 36 MiB in 33 packages=> skopeo copy docker://hello-world oci-archive:image.tar:latest [8.07s]â–• Getting image source signaturesâ–• Copying blob sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54â–• Copying config sha256:811f3caa888b1ee5310e2135cfd3fe36b42e233fe0d76d9798ebd324621238b9â–•â–• Writing manifest to image destinationâ–• Storing signatures=> import {{thunk 20UE3J608LR18: skopeo copy docker://hello-world oci-archive:image.tar:latest}}/image.tar [0.01s]=> oci-layout://load/image.tar@sha256:75ab15a4973c91d13d02b8346763142ad26095e155ca756c79ee3a4aa792991f CACHED [0.00s]-> resolve load/image.tar@sha256:75ab15a4973c91d13d02b8346763142ad26095e155ca756c79ee3a4aa792991f [0.01s]=> /hello [0.17s]â–•â–• Hello from Docker!â–• This message shows that your installation appears to be working correctly.â–•â–• To generate this message, Docker took the following steps:â–• 1. The Docker client contacted the Docker daemon.â–• 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.â–• (amd64)â–• 3. The Docker daemon created a new container from that image which runs theâ–• executable that produces the output you are currently reading.â–• 4. The Docker daemon streamed that output to the Docker client, which sent itâ–• to your terminal.â–•â–• To try something more ambitious, you can run an Ubuntu container with:â–• $ docker run -it ubuntu bashâ–•â–• Share images, automate workflows, and more with a free Docker ID:â–• https://hub.docker.com/â–•â–• For more examples and ideas, visit:â–• https://docs.docker.com/get-started/
null
Thunks can be chained together using from
- this sets each thunk's image to the thunk preceding it, starting from an initial image. Chained thunks propagate their initial working directory from one to the next.
{
:image |
{
} |
||||||||||||||||||
:args |
(
) |
}
When a thunk has another thunk as its image, the deepest thunk determines the runtime. There is currently no meaning for chained Bass thunks, but if you have an idea I'm all ears!
{
:image |
{
} |
||||||||||||||
:args |
(
) |
}
./artist
Thunk paths can passed to other thunks as first-class values. The runtime will handle the boring mechanical work of mounting it into the container in the right place.
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> touch ./artist [0.20s]=> ls {{thunk TGNR3BUA7L4K0: touch ./artist}}/artist [0.21s]â–• ./TGNR3BUA7L4K0/artist
true
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 CACHED [0.00s]-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 [0.01s]=> touch ./artist CACHED [0.00s]=> stat {{thunk TGNR3BUA7L4K0: touch ./artist}}/artist [0.19s]â–• File: ./TGNR3BUA7L4K0/artistâ–• Size: 0 Blocks: 0 IO Block: 4096 regular empty fileâ–• Device: b0h/176d Inode: 60205979 Links: 1â–• Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)â–• Access: 1985-10-26 08:15:00.000000000 +0000â–• Modify: 1985-10-26 08:15:00.000000000 +0000â–• Change: 2023-04-18 04:59:40.120013853 +0000
null
Paths from a hermetic thunk are reproducible artifacts. They can be emit
ted as JSON, saved to a file, and fed to bass --export
to reproduce the artifact.
(emit touchi/artist *stdout*)
{
"thunk": {
"image": {
"thunk": {
"image": {
"ref": {
"platform": {
"os": "linux",
"arch": "amd64"
},
"repository": "alpine",
"tag": "latest",
"digest": "sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"
}
}
}
},
"args": [
{
"string": {
"value": "touch"
}
},
{
"filePath": {
"path": "artist"
}
}
]
},
"path": {
"file": {
"path": "artist"
}
}
}
null
{
:image |
{
} |
||||||||||||||
:args |
(
) |
||||||||||||||
:mounts |
(
) |
}
http
Like thunk paths, thunk addrs can passed to other thunks as first-class values. The runtime will start the service thunk and wait for its ports to be healthy before running the dependent thunk.
{
:image |
{
} |
||||||||||||||||||||||||
:args |
(
) |
}
A value which can be paired with another value to perform some computation and return another value.
Many value types in Bass are also combiners:
{
:dir |
./foo/bar/ |
||||||
:file |
{
} |
||||||
:symbol |
42 |
||||||
:thunk |
{
} ./foo/ |
||||||
:thunk-dir |
{
} ./foo/bar |
||||||
:thunk-file |
{
} |
}
An applicative combiner which takes a list of values as arguments.
A pair or cons whose second element is an empty list or a list.
A pair form evaluates by combining its first value its second value - meaning the first value must be a combiner.
(list 1 2 3)
(
1
2
3
)
[1 2 3]
(
1
2
3
)
(def values [1 2 3])
values
(+ & values)
6
[-1 0 & values]
(
-1
0
1
2
3
)
A process is hermetic if it precisely controls all inputs that may change its result.
In the context of Bass, this is a quality desired of thunks so that artifacts that they produce are reproducible.
First: it is very hard to achieve bit-for-bit reproducible artifacts. Subtle things like file modification timestamps are imperceptible to users but will break checksums nonetheless. Bass normalizes timestamps in thunk output directories to 1985 once the command finishes, but it can't do anything to prevent it while the command is running.
Bass leverages hermetic builds and whenever possible, but it doesn't provide a silver bullet for achieving them, nor does it enforce the practice. It is up to you to make sure you've specified all inputs to whatever level of granularity you want - which may change over time.
The more hermetic you make your thunks, the more reproducible your artifacts will be.
An internal component used for running thunks, configured by the user. A thunk's image determines which runtime is used to run it.
If a thunk does not specify an image, it targets the Bass runtime which evaluates its command as a script. A command path refers to a stdlib module, a host path refers to a script on the local machine, and a thunk path refers to a script created by a thunk.
(
{
:args |
(
) |
}
{
:args |
(
) |
}
{
:args |
(
) |
}
)
If a thunk has an image, its platform selects the runtime, which uses the image as the root filesystem and initial environment variables for the thunk's command. A command path refers to an executable on the $PATH
, a file path refers to a local path in container's filesystem, and a thunk path refers to a file created by a thunk.
(
{
:image |
{
} |
||||||||||||||
:args |
(
) |
}
{
:image |
{
} |
||||||||||||||
:args |
(
) |
}
{
:image |
{
} |
||||||||||||||||||||||||||||||
:args |
(
) |
}
)