google / codeworld

Educational computer programming environment using Haskell
http://code.world
Apache License 2.0
1.24k stars 193 forks source link

Embedded unit-tested examples in source code #1094

Open cdsmith opened 5 years ago

cdsmith commented 5 years ago

If we can get Template Haskell working, we should add a TH-based unit test system that can be embedded into the code. Though, this might be complicated to do with staging restrictions.

cdsmith commented 5 years ago

I think the better way to do this would be to use a source plugin to:

  1. Parse the module with some stolen syntax for examples.
  2. Extract the stolen example syntax, stash examples in a global IORef, and remove them from the parse tree.
  3. In a later stage, inject code to check the condition at runtime, before calling the user's main.

Perhaps this would look something like:

f :: Number -> Number
f(x) = 8 * x + 2

Examples
    | f(0) = 2
    | f(5) = 42

That's a little ugly, as all stolen syntax is. Notice that the source syntax is initially parsed as a definition with guards, but it can be rewritten into a comparison for equality by a source plugin that runs after parsing or renaming.

I'd really want a colon instead of |, but that would be a syntax error because f(5) isn't a pattern. I suppose one could start out with a colon there in the original source, and then do something hacky to replace it with | before GHC sees the code. But I need to decide exactly how far down that path I'm willing to go.

cdsmith commented 5 years ago

Of course, the other option is:

f :: Number -> Number
-- Example: f(5) = 42
f(x) = 8 * x + 2

A plugin could just as easily parse this and make it execute at runtime. It's just a little hokey because it's a comment, and students are taught that those don't matter... but now this becomes "except for this one!"

cdsmith commented 5 years ago

Another alternative bit of stolen syntax is to add a generic assert with ==:

f :: Int -> Int
f(x) = 8 * x + 2
Example: f(0) == 2
Example: f(5) == 42

This parses as a TH splice whose value is a list with first element Example and the rest of the list as f(0) = 2. Obviously a type error, but if you enable TemplateHaskell syntax, it is a valid parse, so a source plugin can steal it. The advantage here is that you can be more flexible and write something like:

Example: all([ f(x) > 0 | x <- [0..10] ]

I think I like this one best.

cdsmith commented 5 years ago

Status: I have started to build a GHC plugin for this. It's fairly easy to find and extract the assertions according to this rule. The remaining tasks are less clear:

module Main (main) where
(main, foo) = (putStrLn "Hello", 5)

When I have a decent story for making this work, I'll check in the code and see where it goes.