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 --releaseRun 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 _endThis 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 _startRead 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 _endControl 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-bPrint bar.
git commit --allow-empty -m 'println "bar"'Tag the commit with the foo tag.
git tag fooSwitch back to the default branch (assumed here to be main).
git switch mainSwitch to a new branch for the ping case.
git switch -c ping-bPrint pong.
git commit --allow-empty -m 'println "pong"'Tag the commit with the ping tag.
git tag pingNow, 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 mainAt 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 _endAt 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-bSince 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 _startPrint 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-loopAdd 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^ HEADFinally, we need to add an empty-message ending commit.
git commit --allow-empty --allow-empty-message -m ''
git tag _endA 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".