UnDAG is an esoteric programming language where the program is a
Git repository — the repository does not track the program, but rather is
itself the program. Program instructions are written in the commit messages
of the repository. Execution starts at the commit tagged _start
, and
moves forward in the commit history until the commit tagged _end
.
Control flow is created using branches and tags.
Build.
cargo build --release
Run a repository as a program.
undag <repo>
As mentioned, program instructions are written in commit messages. Each commit
contains one instruction. Instruction invocations are formatted similarly to
shell commands, with the instruction name followed by a space and then
space-delimited arguments, and one can include a space as part of an argument
by escaping it with a backslash (foo\ bar
) or quoting the whole argument
("foo bar"
). This shell-like behavior means that text is interpreted as
strings by default, regardless of whether it is quoted. This includes numerical
arguments. The value of a variable can be used as an argument by prefixing
the variable name with $
($foo
). Note, however, that unlike with shell
commands, variable access via $
cannot be performed within a string
argument to interpolate the value of the variable into the string
(INVALID: "foo $bar baz"
); variable accesses must be standalone arguments.
As mentioned, even numerical arguments are interpreted as strings by default;
however, variables can be set to numerical values by prefixing the number
with #
(#16
). Currently, only integers are supported.
This will print Hello, world!
and a newline.
Create a commit whose message is println "Hello, world!"
.
git commit --allow-empty -m 'println "Hello, world!"'
Since this is the only commit in the program, it is the start and the end
commit, so give it the _start
and _end
tags.
git tag _start
git tag _end
This will ask for the user's name, then greet them with Hello, <name>!
.
Ask the user for their name.
git commit --allow-empty -m 'println "What is your name?"'
Tag the first commit with _start
git tag _start
Read a line from stdin using inpln
and store it in the name
variable.
git commit --allow-empty -m 'inpln name'
Use concat
to join Hello,
and the inputted name, and store the result in
the message
variable.
git commit --allow-empty -m 'concat message "Hello, " $name'
Use concat
to join !
to the end of the current value of message
, and
store the result back in the message
variable.
git commit --allow-empty -m 'concat message $message "!"'
Lastly, print the value stored in the message
variable.
git commit --allow-empty -m 'println $message'
Tag the end commit with _end
.
git tag _end
Control flow can be created using Git branches, tags, and the branch
instruction. The branch
instruction takes the name of a tag as an
argument, and when it is invoked, it will direct execution down the
path that has the shortest distance from the tag, thus moving
"towards" the commit with that tag.
This will ask the user to type "foo" or "ping", then respond to "foo" with "bar", and respond to "ping" with "pong".
Ask the user to type "foo" or "ping", then read a line from stdin and store
it in the input
variable. Tag the first commit with _start
.
git commit --allow-empty -m 'println "Type foo or ping."'
git tag _start
git commit --allow-empty -m 'inpln input'
Use the branch
instruction, passing in the value of input
.
git commit --allow-empty -m 'branch $input'
Now, execution will attempt to move in the direction of the tag whose name was inputted by the user.
Switch to a new branch for the foo
case. Here, the name foo-b
is being
used to prevent ambiguity with the future foo
tag.
git switch -c foo-b
Print bar
.
git commit --allow-empty -m 'println "bar"'
Tag the commit with the foo
tag.
git tag foo
Switch back to the default branch (assumed here to be main
).
git switch main
Switch to a new branch for the ping
case.
git switch -c ping-b
Print pong
.
git commit --allow-empty -m 'println "pong"'
Tag the commit with the ping
tag.
git tag ping
Now, the two branches must converge to the ending commit. Switch back to the
foo
case branch, move the main
branch there, and switch to it.
git switch foo-b
git branch -f main
git switch main
At this point, since there are no more steps that the program needs to
complete before finishing, the ending commit message should be empty.
Since git merge
does not allow the --allow-empty-message
flag,
the --no-commit
flag must be used, followed by a commit using the
--allow-empty-messaage
flag.
git merge ping-b --no-commit
git commit --allow-empty-message -m ''
Tag the end commit with _end
.
git tag _end
At this point, foo-b
and ping-b
can be deleted, but do not delete
the foo
and ping
tags.
git branch -d foo-b ping-b
Since execution moves forward through Git history, it may seem unclear how
looping may occur. Looping is, in fact, achieved through the use of
git replace
with the --graft
flag
to (somewhat) create cyclic commit histories, since git replace --graft
allows specifying a commit to have any arbitrary list of parents. Of course,
branching must be used to specify whether a loop exits or continues looping.
This will use a loop to print the numbers from 0 to 10.
Use set
to create a counter starting at 0. Note the usage of #
to make
it an integer value. Tag the starting commit with _start
.
git commit --allow-empty -m 'set count #0'
git tag _start
Print the value of count
. This will be the first commit in the loop, so give
it a tag, such as counter-loop
.
git commit --allow-empty -m 'println $count'
git tag counter-loop
Add 1 to the value of count
, and store the result back in count
.
git commit --allow-empty -m 'add count $count #1'
Use gt
to check if count
is greater than 10, setting the end
variable
to 1 if it is, and 0 otherwise.
git commit --allow-empty -m 'gt end $count #10'
We now need to use the 0-or-1 value of end
to choose a tag to send execution
towards. For this, we can use match
. Use match
to set path
to
counter-loop
if end
is 0, and _end
if end
is 1.
git commit --allow-empty -m 'match path $end #0 counter-loop #1 _end'
Use branch
to send execution in the direction of the chosen tag.
git commit --allow-empty -m 'branch $path'
Now, in order to allow execution to move from this commit back to the start
of the loop, we need to add this commit as a parent of the commit at the
start of the loop by using git replace --graft
.
git replace --graft counter-loop counter-loop^ HEAD
Finally, we need to add an empty-message ending commit.
git commit --allow-empty --allow-empty-message -m ''
git tag _end
A table can be considered a scope for variables. There is one global table
which is where all variables are stored by default. However, a variable can
itself store a sub-table which holds its own variables. The variables in a
sub-table can be accessed in multiple ways. The first way is by joining the
variable's name in its sub-table to the name of the sub-table, separated with
/
, and accessing it as a variable. For example, the variable name foo/a
corresponds to the variable named a
in the table foo
. The second way to
access a variable in a sub-table is by "entering" the sub-table. This shifts
the current scope to the sub-table, so that operations that would otherwise
operate on the global table instead operate on the subtable. For example,
enter foo
will cause the variable name a
to represent the variable named
a
in the sub-table foo
, rather than in the global table. The exit
instruction will shift from the current sub-table into its parent table.
More example programs (without explanations) can be found in the examples directory in the form of shell scripts containing Git commands which will generate the program repository.
Invocation | Description |
---|---|
set <var> <src> |
Set the variable named var to the value given by src . |
get <var> <src> |
Set the variable named var to the value of the variable named src . |
del <var> |
Delete the variable named var . |
exists <var> <symbol> |
Set the variable named var to 1 if a variable named symbol exists, and 0 otherwise. |
branch <tag> |
Send execution in the direction of the shortest path to the commit tagged with tag . |
enter <table> |
Change current table to table . |
exit |
Change current table to parent of current table. |
match <var> <src> [<branch> <val>]... |
Find the first value of branch equal to the value given by src , then set var to the corresponding val . |
print <arg> |
Print the value given by arg to stdout, without a trailing newline. |
println <arg> |
Print the value given by arg to stdout, with a trailing newling. |
inpln <var> |
Read a line from stdin, trimming the trailing newline, and store the result in var . |
concat <var> <a> <b> |
Concatenate the string representations of a and b , storing the result in var . |
chars <var> <string> |
Separate string into characters, create a table with variables that store the characters whose names correspond to the character indices and a len variable with the number of characters, and store the result in var . |
eq <var> <a> <b> |
Set var to 1 if a and b are equal, and 0 otherwise. |
gt <var> <a> <b> |
Set var to 1 if a is greater than b , and 0 otherwise. |
add <var> <a> <b> |
Add a and b , storing the result in var . |
sub <var> <a> <b> |
Subtract b from a , storing the result in var . |
mul <var> <a> <b> |
Multiply a and b , storing the result in var . |
div <var> <a> <b> |
Divide a by b , storing the result in var . |
mod <var> <a> <b> |
Perform a modulo on a and b , storing the result in var . |
and <var> <a> <b> |
Perform a bitwise "and" on a and b , storing the result in var . |
or <var> <a> <b> |
Perform a bitwise "or" on a and b , storing the result in var . |
xor <var> <a> <b> |
Perform a bitwise "xor" on a and b , storing the result in var . |
Git histories are Directed Acyclic Graphs,
which have no cycles. This property, however, is bypassable through the usage
of git replace --graft
, which is what this esoteric programming language
does, hence "Un-DAG".