expressions

I've been forging ahead with stabilizing this latest set of tools and amending the examples in these posts as I go without stopping to think about what changed. Most notably ursula now has a brother, poseidon, so they inhabit a shared repository. That also means I'm deleting the original ursula repository, a flicker of a primordial hadron struggling to take shape before collapsing and bubbling back up in some new form. So update your bookmarks!

I've pulled a large chunk of shared logic out of ytree and execvm and moved it into metrologyf, where I define some helper functions plus a small suite of abstract and base classes for describing computations. This marks an occasion to think about what computation even is.

In this ecosystem the value of a statement is determined by the execution of an expression, and intentionally or not our agreed use of the term "execution" seems appropriately violent for the purpose. A statement marks a pivot point of identity between one value and another, the membrane between a state transfer. But then the statement is itself an expression subject to execution, so its distinction as something different seems like a contrivance. Maybe we can resolve that with one more layer of abstraction! And if the layers of identity ever become insurmountable the easy way out is to introduce another god of identity in conflict with the other.

In most cases so far the point hasn't been all that interesting. Both ytree.tar and execvm.ysh look like filter functions that operate on streams of documents. For those tools, the "top level" documents in the input stream are statements (expressions to be executed) and their evaluation is the "typical thing", which for execvm.ysh means deferring to execv. For ytree.tar that means either "generate a tar-encoded stream of output" or "extract to the filesystem". I previously only supported that first outcome, but extraction turned out to be relatively painless to add (although it's not entirely complete so I wouldn't trust it not to bonk your data).

I've amended the runtime arguments to look more like the typical tar. A combination of dry-run and verbosity settings also affect the operation. I don't really know file sizes ahead of time (not without executing something!) so I left room for uncertainty there.

$ python -m ytree.tar --create scripts/entries/string.yaml \
      | tar -tvf -
drwxr-x--- 0/0               0 1969-12-31 19:00 ./build/scripts/string
-rw-r----- 0/0              12 1969-12-31 19:00 ./build/scripts/string/1
-rw-r----- 0/0              26 1969-12-31 19:00 ./build/scripts/string/2

$ python -m ytree.tar --create -nv scripts/entries/string.yaml
drwxr-x--- 0/0 *************** 1969-12-31 19:00 ./build/scripts/string
-rw-r----- 0/0 *************** 1969-12-31 19:00 ./build/scripts/string/1
-rw-r----- 0/0 *************** 1969-12-31 19:00 ./build/scripts/string/2

$ python -m ytree.tar --create -nvv scripts/entries/string.yaml
drwxr-x--- 0/0               0 1969-12-31 19:00 ./build/scripts/string
-rw-r----- 0/0              12 1969-12-31 19:00 ./build/scripts/string/1
-rw-r----- 0/0              26 1969-12-31 19:00 ./build/scripts/string/2

The evaluation loop invites each resource to call out to Some Protocol to do the actual execution work as the resources are loaded. Extraction (in this case a dry-run extraction so I have something to show you here) differs from creation only in the selection of protocol.

$ python -m ytree.tar -xn scripts/entries/string.yaml
./build/scripts/string
./build/scripts/string/1
./build/scripts/string/2

Now it's a straightforward thing that execvm.ysh is nearly identical, differing in both the types of resources he knows how to input and the protocol he supplies to them.

$ python -m execvm.ysh scripts/hello.yaml
hello world

$ python -m execvm.ysh -n scripts/hello.yaml
echo hello world

$ python -m execvm.ysh -nv scripts/hello.yaml
--- !execvm/posix/command/v1
argv: [echo, hello, world]
cwd: null
env: {}
stderr: !execvm/posix/stream/v1 stderr
stdout: !execvm/posix/stream/v1 stdout

One of the abstractions metrologyf provides is an orchestration layer over lupa for wrangling lua runtimes. His evaluation is again nearly identical but for the added challenge of having to be explicit about synchronization. Each endpoint exposed to lua is backed by a python function that returns Some Object, which lets me pass those objects around in lua. In this example, ursula.env() returns a string, execvm.posix.execv() returns a command object, and execvm.posix.stream() returns a stream object.

foo = ursula.env "FOO"

ursula.execv {
   argv = {"echo", "hello", foo},
   stdout = execvm.posix.execv {
      argv = {"cat", "-"},
      stdout = execvm.posix.stream "stderr"
   }
}

And then there's the seeming asymmetry of ursula.execv. Under the hood he defers to the same execvm.posix.execv endpoint to construct a command, which he executes immediately. The protocol I use here returns the same value because gifting witches a piece of myself just to get the same thing back sounds like my kind of magic.

def command(self, table):
    return command(...)

...

def execv(self, table):
    return self.lua.protocol.execute(
        execvm.command(table)
    )

Plus it makes things easier. Since the value of ursula.execv is a command object, it's valid to use anywhere that expects a command even after its execution. What value in destruction!

ursula.execv {
   argv = {"echo", "bar"},
   stdout = ursula.execv {
      argv = {"cat", "-"},
      stdout = execvm.posix.stream "stderr"
   }
}

This probably isn't a typical usage of the language, but I can't not wonder what could be expressed. That script first serializes the child command and then the parent, which we can say includes a copy of the child even though we all know it refers to the same object. How could it not? All the constructor parameters are the same!

$ python -m dynastyf.ursula scripts.nested
--- !execvm/posix/command/v1
argv: [cat, '-']
cwd: null
env: {}
stderr: !execvm/posix/stream/v1 stderr
stdout: !execvm/posix/stream/v1 stderr
--- !execvm/posix/command/v1
argv: [echo, bar]
cwd: null
env: {}
stderr: !execvm/posix/stream/v1 stderr
stdout: !execvm/posix/command/v1
  argv: [cat, '-']
  cwd: null
  env: {}
  stderr: !execvm/posix/stream/v1 stderr
  stdout: !execvm/posix/stream/v1 stderr

I need to pass data on stdin to run that script so I can't pipe the script itself into execvm.ysh. This is actually illuminating a portion of the API I'd rather tweak so that I can better supply both scripts and data without needing a filesystem, but our vocabulary barely has morphemes. So here I'm simply using bash magic, the chip clip at the edge of the universe holding it all together.

$ echo foo \
      | python -m execvm.ysh <(
          python -m dynastyf.ursula scripts.nested
      )
foo
bar

So far all of the ursula examples I've shown generate either ytree or execvm objects, but there's nothing to stop her from conjuring what you ask for, which might be some combination of both.

local ursula = require("ursula")
local ytree = require("ytree")

return function (values)

   ursula.fs.file {
      name = "foo",
      contents = ytree.res.string(
         string.format("hello, %s", values.domain)
      )
   }

   ursula.execv {
      argv = {"cat", "foo"}
   }

end

Nothing except your needing to ask her to begin with. Spells, Ursula, please!

$ python -m dynastyf.ursula \
         --values files/values.yaml \
         files.version
--- !ytree/tar/file/v1
name: files/version.yaml
contents: !ytree/res/string/v1
  value: expressions
--- !execvm/posix/command/v1
argv: [echo, updated version to, expressions]
cwd: null
env: {}
stderr: !execvm/posix/stream/v1 stderr
stdout: !execvm/posix/stream/v1 stdout

One thing is comfortable but it's difficult to grow without a dialectic. Two things is fruitful but dependency cycles become unwieldy. A trinity is a longtime crowd pleaser, and with some care around the boundaries we can manifest even more expressive functions. Neither execvm nor ytree has awareness of the other's types, but both sets of types are built on abstractions defined in metrologyf, the language that empowers and constricts the two. So long as both are reasonably mutually exclusive it's not all that difficult to define a union of the two, the reaction from which poseidon emerged. He takes the same set of arguments as both tools combined and implements a protocol that defers to the protocols defined by each individual library. With all of that in place I can finally write a sentence like this.

$ python -m dynastyf.ursula \
         --values files/values.yaml \
         files.ursula.expressions \
      | python -m dynastyf.poseidon
hello, https://yieldsfalsehood.com