Skip to content

Instantly share code, notes, and snippets.

@pchiusano
Last active August 19, 2019 18:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pchiusano/b4607611089498b777c8e7f69c054765 to your computer and use it in GitHub Desktop.
Save pchiusano/b4607611089498b777c8e7f69c054765 to your computer and use it in GitHub Desktop.
Unison transcript files (RFC draft)

Unison transcript files

This is a Unison transcript file. It's used for creating typechecked documentation, test cases, and scripts.

It's just a markdown file that does some special interpretation of certain fenced code blocks. It can be passed to ucm as a command line argument. ucm will run through the file change events and commands in the file in order and output a new .md file with the responses to these inputs. It exits when it's done, so this can also be used for scripting.

Basics

---
filename: scratch.u
---
x = 1 + 1

The above will create the file scratch.u with the given contents, parse and typecheck it, and insert the ucm output into the resulting markdown, as a new fenced code block, immediately following the unison fenced block. If you leave off the frontmatter with the filename, that's okay too, no file will be created. If the file doesn't parse or typecheck, ucm exits with an error.

If you are instead expecting an error and want to show the resulting error message and continue, use unison:error as the code fence:

x = 1 + "hi" -- thankfully does not typecheck

Another fenced code type is ucm (ucm:error if you are expecting an error):

.> view base.List.map
.> execute IO.putText "hello, world!!"

This will run the ucm command and show its output in a new fenced code block immediately below. A ucm fenced block can have multiple commands, in which case the output is inserted after each command.

You can elide the cd/namespace commands and just directly indicate the namespace with the prompt, for instance:

.foo> find
.bar> find

This will output the command sequence cd .foo, find, cd .bar, find.

Creating tests cases

Sounds good, now what about creating test cases? We provide a couple new commands:

  • assert.execute expr exits ucm with failure if execute expr fails or evaluates to false.
  • assert.test exits ucm with failure if the test command returns a failing status
  • assert.todo exits ucm with failure if the todo command returns anything to do
  • assert.view foo exits ucm with failure if view command gives no results
  • ... can add to this as needed
.> assert true
.> assert.test

Examples

It might not be obvious how to use the above to construct test cases, so I thought I'd give a few examples. Suppose I'd like to check that fork creates a copy of a namespace. To make the transcript pretty self-contained, I'll create a namespace with some definitions in it, then fork that, cd into it, and verify that viewing one of the definitions works.

ns0.x = 42
ns0.y = 19
.> add
.> fork ns0 ns1
.ns1> assert.view x
.ns1> assert.view y

In the absence of a full-blown codebase API accessible from Unison programs, there may be some creativity needed to arrange things so that the transcript fails in a way that indicates what you want. The above test is a bit weak, it would be nice if we could assert that the x in the fork is the same as the x in ns0. How could we do that? Easy: just write a Unison test!

use .test
use .base

test> ns1.forkIsCopy1 = (.ns0.x == .ns1.x) |> expect |> run
test> ns1.forkIsCopy1 = (.ns0.y == .ns1.y) |> expect |> run
.> add
.> assert.test

Now we're actually testing that the values are as expected in the fork.

@pchiusano
Copy link
Author

pchiusano commented Aug 19, 2019

• what if you want to add some code and show it in your doc, but don't want to show the reader the ucm output?
• similarly, what if you want to add some code the codebase (a helper function say) but don't want to show it in the doc?
• similarly, give an example of some failing code but not show the compile error
I guess with these I'm suggesting unison:hide, unison:hide-ucm, unison:error:hide, unison:error:hide-ucm, and ucm:hide. (or 'hidden' maybe)

Seems reasonable.

assert.todo is a slightly surprising name for a function that checks we don't have any todo. Similarly assert.view is a bit of a surprising name for checking that view returns nonempty results.

Ideas?

• "failure if execute expr fails or evaluates to false." can't execute only take stuff of type () at the moment?

I think that is not enforced right now, but it probably should be. So I guess just if the expression fails then.

• what if you want to show some code, and add it to the codebase, without showing a filename in your doc? (it's not essential that this be possible, I guess)

Just don't include the filename in the front-matter of the markdown.

• can I start from a codebase that I pull, to use that as the basis of the transcript? maybe with a pull command inside a ucm block

Yes, just do pull to start things out.

• is there any way for me to keep a codebase on github somewhere in sync with the results of this transcript? e.g. so people can poke around with my ability examples? can I load a transcript in ucm and be dropped into the prompt so I can push it myself?

I guess you could add a push at the end of the transcript. Or maybe a flag passed to ucm that drops you into the regular prompt once the transcript completes.

• any way to preserve unison's output text colouring in markdown?

Seems doable - you could just emit HTML blocks manually in the markdown. Maybe not for v1.

• does a use .base.io in one fence persist to the next? can I reset that somehow?

No. Only state is just the codebase state. All the unison fenced blocks are syntactically self-contained. You can add to make previous definitions available for later files. However, if needed maybe we could add some "stitched" unison fenced block series, which is combined into a single unison fenced block. I guess I'd prefer to wait on that until it's clear we need it though.

• if I'm building a reference made up of lots of little docs, can I say that one of them 'follows on' from another, to avoid having to repeat all the definitions that set things up?

I'd probably just start the transcript with a pull of the codebase you want to depend on. Seems like a rabbit hole to have any more elaborate transcript dependency / build system than this.

@pchiusano
Copy link
Author

in the rendered result are the fences still going to be haskell so that github can do something useful with them?

I'd say no, or maybe it could be a config option. I'd rather host the markdown files on a site that can render unison fenced blocks, like a github pages site with some custom syntax highlighting.

@atacratic
Copy link

assert.todo.empty, assert.view.succeeds ?

"I guess just if the expression fails then." OK, although not actually sure what you mean by 'fails'. Maybe that it can't parse expr?

"Just don't include the filename in the front-matter of the markdown." does that rule out adding it to the codebase? If not then fine.

"Or maybe a flag passed to ucm that drops you into the regular prompt once the transcript completes." I like this idea.

Allowing push from transcript possibly a bit fraught? parsing a transcript then has a side-effect? Maybe it's fine.

"No. Only state is just the codebase state. All the files are self contained." It's going to be a bit of a yawn if all the tiny fragments in the abiities tutorial have to repeat the use .base use .base.io incantation (or do without them).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment