Bass

bassics

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.

scalar values

boolean
𝄢

true or false, but sometimes null.

𝄢

Boolean values are pretty straightforward - the only catch is that null also counts as false when given to (if) or (not).

𝄢

Otherwise, all values - including "empty" ones - are truthy.

[(if true :truthy :falsy)
 (if false :truthy :falsy)
 (if null :truthy :falsy)
 (if [] :truthy :falsy)
 (if "" :truthy :falsy)
 (if _ :truthy :falsy)]
(
  1. truthy
  2. falsy
  3. falsy
  4. truthy
  5. truthy
  6. truthy
)
number
𝄢

An integer value. Floating point values are not supported.

(* 6 7)
42
string
𝄢

A UTF-8 immutable string value.

𝄢

TODO: document escape sequences

"hello, world!"
"hello, world!"
symbol
𝄢

A name, typically bound to a value in a scope.

𝄢

A symbol form evaluates by fetching its binding in the current scope.

(wrap <builtin: (symbol? val)>)
𝄢

Symbols may be constructed using a keyword form, analogous to cons forms which construct pairs.

: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. 1
  2. 2
  3. end
)
𝄢

Symbols may be chained together with keyword notation to traverse scopes.

(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)
]
(
  1. 2
  2. 123
  3. 42
)
keyword
𝄢

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!
𝄢

Keywords go hand-in-hand with cons, which is used to construct pairs.

[:+ 1 2 3]
(
  1. +
  2. 1
  3. 2
  4. 3
)
𝄢

Note: keywords evaluate to a symbol - they are not a distinct value.

empty list
𝄢

An empty list is represented by () or [], which are both the same constant value.

(= [] ())
true
null
𝄢

Everyone's favorite type. Used to represent the absense of value where one might typically be expected.

null
null
𝄢

Note: null is a distinct type from an empty list. The only (null?) value is null, and the only (empty?) value is [].

(map null? [[] (list) null false])
(
  1. false
  2. false
  3. true
  4. false
)
ignore
𝄢

_ (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!
_
_

data structures

scope
𝄢

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.

(defop here _ scope scope)

(let [local-binding "hello"]
  (here))
{
:local-binding "hello"
{
:*memos* <host: bass.lock>
:here (op _ scope scope)
{
:*dir* <host: /tmp/nix-shell.fUJHAA/bass-scope1634964617>
:*env*
{
}
:*stdin* <source: empty>
:*stdout* <sink: empty>
:main (wrap <builtin: (main)>)
<scope: ground>
}
}
}
bind
𝄢

{bind} notation is a scope literal acting as a map data structure.

(eval [str :uri "@" :branch]
  {:uri "https://github.com/vito/bass"
   :branch "main"})
"https://github.com/vito/bass@main"
𝄢

Parent scopes may be provided by listing them anywhere in between the braces. For example, here's a scope-based alternative to (let):

(defop with [child & body] parent
  (eval [do & body] {(eval child parent) parent}))

(with {:a 1 :b 2}
  (+ a b))
3
𝄢

Comments within the braces are recorded into the child scope, enabling their use for lightweight schema docs:

(eval [doc]
  {; hello world!
   :foo "sup"

   ; goodbye world!
   :bar "later"})
stderr: 9 lines
--------------------------------------------------
foo string?
hello world!
--------------------------------------------------
bar string?
goodbye world!
null
pair
𝄢

A list of forms wrapped in (parentheses), or constructed via the cons function or cons notation.

(= (cons 1 (cons 2 [])) (list 1 2))
true
𝄢

A & may be used to denote the second value instead of terminating with an empty list.

(= (cons 1 2) (list 1 & 2))
true
cons
𝄢

A list of forms wrapped in [square brackets].

(= (cons 1 (cons 2 [])) [1 2])
true
𝄢

A & may be used to denote the second value instead of terminating with an empty list.

(= (cons 1 2) [1 & 2])
true
operative
𝄢

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)
(
  1. (
    1. a
    2. b
    3. c
    )
  2. {
    :*memos* <host: bass.lock>
    :quote-with-scope (op args scope [args scope])
    {
    :*dir* <host: /tmp/nix-shell.fUJHAA/bass-scope2342072925>
    :*env*
    {
    }
    :*stdin* <source: empty>
    :*stdout* <sink: empty>
    :main (wrap <builtin: (main)>)
    <scope: ground>
    }
    }
)
applicative
𝄢

A combiner which wraps an underlying operative and evaluates its operands before passing them along to it as arguments.

𝄢

Applicatives, typically called functions, are defined with the (defn) operative or constructed with (fn).

(defn inc [x]
  (+ x 1))

(inc 41)
42
source
𝄢

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. 1
  2. 2
  3. 3
)
𝄢

A source is also returned by (run) to pass along values emitted by the thunk.

When (next) hits the end of the stream, an error will be raised. A default value may be supplied as the second argument to prevent erroring.

sink
𝄢

A destination for values which may be sent with (emit).

𝄢

All scripts can emit values to the *stdout* sink, which encodes values as JSON to stdout.

(emit "hello!" *stdout*)
(emit 42 *stdout*)
"hello!"
42
null

paths

path
𝄢

A location of a file or directory within a filesystem.

𝄢

Bass distinguishes between file and directory paths by requiring a trailing slash (/) for directories.

(def file ./some-file)
(def dir ./some-dir/)
[file dir]
(
  1. ./some-file
  2. ./some-dir/
)
𝄢

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
dir path
𝄢

A path to a directory, possibly in a certain context.

A context-free file path looks like ./foo/ - note the presence of a trailing slash.

When passed to a path root to form a subpath, the root determines the directory's context.

file path
𝄢

A path to a file, possibly in a certain context.

A context-free file path looks like ./foo - note the lack of a trailing slash.

When passed to a path root to form a subpath, the root determines the file's context.

command path
𝄢

A name of a command to be resolved to an actual path to an executable at runtime, using $PATH or some equivalent.

.cat
.cat
host path
𝄢

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/nix-shell.fUJHAA/bass-scope2205039609>
𝄢

Host paths can be passed into thunks with copy-on-write semantics.

(-> ($ ls $*dir*)
    (with-image (linux/alpine))
    run)
stderr: 10 lines
=> docker-image://docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697
984fba772b3976835194c6d4 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba7
72b3976835194c6d4 [0.01s]
=> resolve image config for docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e48
0fef81e697984fba772b3976835194c6d4 [0.01s]
=> local:///tmp/nix-shell.fUJHAA/bass-scope2205039609 [0.01s]
-> transferring /tmp/nix-shell.fUJHAA/bass-scope2205039609: 2B [0.00s]
=> copy / / CACHED [0.00s]
=> ls <host: /tmp/nix-shell.fUJHAA/bass-scope2205039609> [0.22s]
null
path root
𝄢

A combiner that can be called with a path argument to return another path. Typically used with path extending notation.

Any dir path path is a path root for referencing files or directories beneath the directory.

(def thunk-dir
  (subpath (.foo) ./dir/))

(def host-dir
  *dir*/dir/)

[thunk-dir/file
 host-dir/file
 ./foo/bar/baz
 ((./foo/ ./bar/) ./baz)]
(
  1. .foo
    {
    :cmd .foo
    }
    ./dir/file
  2. <host: /tmp/nix-shell.fUJHAA/bass-scope3913404514/dir/file>
  3. ./foo/bar/baz
  4. ./foo/bar/baz
)

thunks & paths

thunk
𝄢

A serializable object representing a command to run in a controlled environment.

(from (linux/alpine)
  ($ echo "Hello, world!"))
.echo
{
:image
{
:repository "alpine"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
}
:cmd .echo
:args
(
  1. "Hello, world!"
)
}
𝄢

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 used or loaded in addition to being run.

(.git (linux/alpine/git))
.git
{
:cmd .git
:stdin
(
  1. {
    :repository "alpine/git"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
    }
)
}
image
𝄢

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)
{
:repository "alpine"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
}
𝄢

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)))
]
(
  1. .echo
    {
    :image
    {
    :repository "alpine"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
    }
    :cmd .echo
    :args
    (
    1. "Hello, world!"
    )
    }
  2. .echo
    {
    :image
    {
    :repository "alpine"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
    }
    :cmd .echo
    :args
    (
    1. "Hello, world!"
    )
    }
  3. .echo
    {
    :image
    {
    :repository "alpine"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
    }
    :cmd .echo
    :args
    (
    1. "Hello, world!"
    )
    }
)
𝄢

To reference an OCI image archive created by a thunk, set :file to a thunk path.

(def hello-oci
  (subpath
    (from (linux/alpine)
      ($ apk add skopeo)
      ($ skopeo copy "docker://hello-world" "oci-archive:image.tar:latest"))
    ./image.tar))

(def hello-world
  {:file hello-oci
   :platform {:os "linux"}
   :tag "latest"})

(run (from hello-world
       ($ /hello)))
stderr: 92 lines
=> docker-image://docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697
984fba772b3976835194c6d4 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba7
72b3976835194c6d4 [0.01s]
=> resolve image config for docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e48
0fef81e697984fba772b3976835194c6d4 [0.02s]
=> /shim get-config /image.tar latest /config [0.13s]
=> apk add skopeo [1.93s]
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.0-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.2-r0)
(16/18) Installing gpgsm (2.2.40-r0)
(17/18) Installing gpgme (1.18.0-r0)
(18/18) Installing skopeo (1.10.0-r2)
Executing busybox-1.35.0-r29.trigger
OK: 35 MiB in 33 packages
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.0-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.2-r0)
(16/18) Installing gpgsm (2.2.40-r0)
(17/18) Installing gpgme (1.18.0-r0)
(18/18) Installing skopeo (1.10.0-r2)
Executing busybox-1.35.0-r29.trigger
OK: 35 MiB in 33 packages
=> skopeo copy docker://hello-world oci-archive:image.tar:latest [1.01s]
Getting image source signatures
Copying blob sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54
Copying config sha256:811f3caa888b1ee5310e2135cfd3fe36b42e233fe0d76d9798ebd324621238b9
Writing manifest to image destination
Storing signatures
Getting image source signatures
Copying blob sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54
Copying config sha256:811f3caa888b1ee5310e2135cfd3fe36b42e233fe0d76d9798ebd324621238b9
Writing manifest to image destination
Storing signatures
=> /shim unpack /image.tar latest /rootfs [0.15s]
2022/11/28 01:28:21 info unpack layer: sha256:2db29710123e3e53a794f2694094b9b4338aa9e
e5c40b930cb8063a1be392c54
=> /hello [0.21s]
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.

(from (linux/alpine)
  ($ mkdir ./foo/)
  ($ touch ./foo/bar))
.touch
{
:image
.mkdir
{
:image
{
:repository "alpine"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
}
:cmd .mkdir
:args
(
  1. ./foo/
)
}
:cmd .touch
:args
(
  1. ./foo/bar
)
}
𝄢

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!

thunk path
𝄢

A file or directory path relative to the output directory of a thunk.

(def touchi
  (from (linux/alpine)
    ($ touch ./artist)))

touchi/artist
.touch
{
:image
{
:repository "alpine"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
}
:cmd .touch
:args
(
  1. ./artist
)
}
./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.

(succeeds?
  (from (linux/alpine)
    ($ ls touchi/artist)))
stderr: 9 lines
=> docker-image://docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697
984fba772b3976835194c6d4 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba7
72b3976835194c6d4 [0.01s]
=> resolve image config for docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e48
0fef81e697984fba772b3976835194c6d4 [0.01s]
=> touch ./artist [0.21s]
=> ls <thunk BI56UPM6M8C2A: (.touch)>/artist [0.23s]
./BI56UPM6M8C2A/artist
true
𝄢

Thunk path timestamps are normalized to 1985-10-26T08:15:00Z to assist with hermetic builds.

(run (from (linux/alpine)
       ($ stat touchi/artist)))
stderr: 15 lines
=> docker-image://docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697
984fba772b3976835194c6d4 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba7
72b3976835194c6d4 [0.01s]
=> resolve image config for docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e48
0fef81e697984fba772b3976835194c6d4 [0.01s]
=> touch ./artist [0.23s]
=> stat <thunk BI56UPM6M8C2A: (.touch)>/artist [0.23s]
File: ./BI56UPM6M8C2A/artist
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: 73h/115d Inode: 15754192 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: 2022-11-28 01:28:22.742376765 +0000
null
𝄢

Paths from a hermetic thunk are reproducible artifacts. They can be emitted as JSON, saved to a file, and fed to bass --export to reproduce the artifact.

(emit touchi/artist *stdout*)
{
  "thunk": {
    "image": {
      "ref": {
        "repository": "alpine",
        "platform": {
          "os": "linux"
        },
        "tag": "latest",
        "digest": "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
      }
    },
    "cmd": {
      "command": {
        "name": "touch"
      }
    },
    "args": [
      {
        "filePath": {
          "path": "artist"
        }
      }
    ]
  },
  "path": {
    "file": {
      "path": "artist"
    }
  }
}
null
thunk addr
𝄢

A network address to a port served by a thunk, i.e. a service.

(defn http-server [index]
  (from (linux/python)
    (-> ($ python -m http.server)
        (with-mount (mkfile ./index.html index) ./index.html)
        (with-port :http 8000))))

(addr (http-server "Hello, world!") :http "http://$host:$port")
.python
{
:image
{
:repository "python"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:10fc14aa6ae69f69e4c953cffd9b0964843d8c163950491d2138af891377bc1d"
}
:cmd .python
:args
(
  1. "-m"
  2. "http.server"
)
:mounts
(
  1. {
    :source <fs>/index.html
    :target ./index.html
    }
)
}
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.

(defn echo [msg]
  (let [server (http-server msg)]
    (from (linux/alpine)
      ($ wget -O- (addr server :http "http://$host:$port")))))

(echo "Hello, world!")
.wget
{
:image
{
:repository "alpine"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
}
:cmd .wget
:args
(
  1. "-O-"
  2. .python
    {
    :image
    {
    :repository "python"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:10fc14aa6ae69f69e4c953cffd9b0964843d8c163950491d2138af891377bc1d"
    }
    :cmd .python
    :args
    (
    1. "-m"
    2. "http.server"
    )
    :mounts
    (
    1. {
      :source <fs>/index.html
      :target ./index.html
      }
    )
    }
    http
)
}
𝄢

When multiple Bass sessions run the same service thunk they will actually be deduplicated into one instance with output multiplexed to all attached clients. The service will only be stopped when all Bass sessions have finished using it. This is all thanks to Buildkit!

concepts

combiner
𝄢

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 (./foo ./bar/)
 :symbol (:foo {:foo 42})
 :thunk (($ .mkdir ./foo/) ./foo/)
 :thunk-dir ((($ .mkdir ./foo/) ./foo/) ./bar)
 :thunk-file (*dir*/script {:config "hi"})}
{
:dir ./foo/bar/
:file
./foo
{
:cmd ./foo
:stdin
(
  1. ./bar/
)
}
:symbol 42
:thunk
.mkdir
{
:cmd .mkdir
:args
(
  1. ./foo/
)
}
./foo/
:thunk-dir
.mkdir
{
:cmd .mkdir
:args
(
  1. ./foo/
)
}
./foo/bar
:thunk-file
<host: /tmp/nix-shell.fUJHAA/bass-scope650028215/script>
{
:cmd <host: /tmp/nix-shell.fUJHAA/bass-scope650028215/script>
:stdin
(
  1. {
    :config "hi"
    }
)
}
}
function
𝄢

An applicative combiner which takes a list of values as arguments.

list
𝄢

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. 1
  2. 2
  3. 3
)
𝄢

A cons form evaluates like cons: it constructs a pair by evaluating each of its values.

[1 2 3]
(
  1. 1
  2. 2
  3. 3
)
𝄢

Both pair and cons may have a & symbol which provides a value for the rest of the list.

(def values [1 2 3])
values
(+ & values)
6
[-1 0 & values]
(
  1. -1
  2. 0
  3. 1
  4. 2
  5. 3
)
module
𝄢

A scope, typically defined in its own file, providing an interface to external modules that use it.

See provide, import, and load.

hermetic
𝄢

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.

runtime
𝄢

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.

(use (.git (linux/alpine/git)))

[(.strings) (*dir*/foo.bass) (git:github/vito/tabs/ref/main/gh.bass)]
(
  1. .strings
    {
    :cmd .strings
    }
  2. <host: /tmp/nix-shell.fUJHAA/bass-scope2367059486/foo.bass>
    {
    :cmd <host: /tmp/nix-shell.fUJHAA/bass-scope2367059486/foo.bass>
    }
  3. .git
    {
    :image
    .git
    {
    :image
    .git
    {
    :image
    .git
    {
    :image
    {
    :repository "alpine/git"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
    }
    :cmd .git
    :args
    (
    1. "clone"
    2. "https://github.com/vito/tabs"
    3. ./
    )
    }
    :cmd .git
    :args
    (
    1. "fetch"
    2. "origin"
    3. "c97bdc3bc41acb5c1bebec6fba9994ee2fb992a5"
    )
    }
    :cmd .git
    :args
    (
    1. "checkout"
    2. "c97bdc3bc41acb5c1bebec6fba9994ee2fb992a5"
    )
    }
    :cmd .git
    :args
    (
    1. "submodule"
    2. "update"
    3. "--init"
    4. "--recursive"
    )
    }
    ./gh.bass
    {
    :cmd
    .git
    {
    :image
    .git
    {
    :image
    .git
    {
    :image
    .git
    {
    :image
    {
    :repository "alpine/git"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
    }
    :cmd .git
    :args
    (
    1. "clone"
    2. "https://github.com/vito/tabs"
    3. ./
    )
    }
    :cmd .git
    :args
    (
    1. "fetch"
    2. "origin"
    3. "c97bdc3bc41acb5c1bebec6fba9994ee2fb992a5"
    )
    }
    :cmd .git
    :args
    (
    1. "checkout"
    2. "c97bdc3bc41acb5c1bebec6fba9994ee2fb992a5"
    )
    }
    :cmd .git
    :args
    (
    1. "submodule"
    2. "update"
    3. "--init"
    4. "--recursive"
    )
    }
    ./gh.bass
    }
)
𝄢

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.

[(from (linux/alpine)
   (.ls))
 (from (linux/alpine)
   (/bin/date))
 (from (linux/nixos/nix)
   ($ nix-env -f "<nixpkgs>" -iA neovim git)
   (git:github/vito/dot-nvim/ref/main/README.sh))
]
stderr: 10 lines
=> resolve image config for docker.io/nixos/nix:latest [0.58s]
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.02s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.01s]
=> git ls-remote https://github.com/vito/dot-nvim main [0.51s]
=> exporting to client directory [0.00s]
-> copying files 83B [0.00s]
(
  1. .ls
    {
    :image
    {
    :repository "alpine"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
    }
    :cmd .ls
    }
  2. /bin/date
    {
    :image
    {
    :repository "alpine"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
    }
    :cmd /bin/date
    }
  3. .git
    {
    :image
    .git
    {
    :image
    .git
    {
    :image
    .git
    {
    :image
    {
    :repository "alpine/git"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
    }
    :cmd .git
    :args
    (
    1. "clone"
    2. "https://github.com/vito/dot-nvim"
    3. ./
    )
    }
    :cmd .git
    :args
    (
    1. "fetch"
    2. "origin"
    3. "3fab78c6201441cb8cd7f0abba1a511d12f5c497"
    )
    }
    :cmd .git
    :args
    (
    1. "checkout"
    2. "3fab78c6201441cb8cd7f0abba1a511d12f5c497"
    )
    }
    :cmd .git
    :args
    (
    1. "submodule"
    2. "update"
    3. "--init"
    4. "--recursive"
    )
    }
    ./README.sh
    {
    :image
    .nix-env
    {
    :image
    {
    :repository "nixos/nix"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:d8c6b97091d6944dd773c3c239899af047077dbf5411ef229bb50e5b21404b0d"
    }
    :cmd .nix-env
    :args
    (
    1. "-f"
    2. "<nixpkgs>"
    3. "-iA"
    4. "neovim"
    5. "git"
    )
    }
    :cmd
    .git
    {
    :image
    .git
    {
    :image
    .git
    {
    :image
    .git
    {
    :image
    {
    :repository "alpine/git"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
    }
    :cmd .git
    :args
    (
    1. "clone"
    2. "https://github.com/vito/dot-nvim"
    3. ./
    )
    }
    :cmd .git
    :args
    (
    1. "fetch"
    2. "origin"
    3. "3fab78c6201441cb8cd7f0abba1a511d12f5c497"
    )
    }
    :cmd .git
    :args
    (
    1. "checkout"
    2. "3fab78c6201441cb8cd7f0abba1a511d12f5c497"
    )
    }
    :cmd .git
    :args
    (
    1. "submodule"
    2. "update"
    3. "--init"
    4. "--recursive"
    )
    }
    ./README.sh
    }
)
𝄢

The internal architecture is modular so that many runtimes can be implemented over time, but the only non-Bass runtime currently implemented is Buildkit.

platform
𝄢

A scope containing arbitrary bindings used to select the appropriate configured runtime for a given thunk.

stream
𝄢

A stream is a source with a corresponding sink for passing a sequence of values from one end to another.