Skip to content

TM007 Fulfills block and Pyret test mode

Daniel Patterson edited this page May 23, 2013 · 2 revisions

Motivation:

We want it to be easy to write tests / oracles for functions and data. Testing top level functions is easy, but nested functions that might close over variables are harder to test, with the only obvious way to test them being to execute the tests whenever the wrapping function is called, which could significantly slow down program execution. Instead, we propose to add an optional block to every function (and data) that contains arbitrary code, intended to be simple tests, oracles, etc. We propose to call this block "fulfills", to highlight the descriptive nature of the tests. Then we will provide a mode that causes those blocks to be run (and otherwise, they are ignored).

Examples:

fun add2(n :: Number):
  n.plus(2)
fulfills:
  test.expect(add2(2), 4)
  test.expect(add2(0), 2)
  add2oracle(add2)
end

fun square-add3(n :: Number):
  fun square(x :: Number):
    x.times(x)
  fulfills:
    test.expect(square(-1), 1)
    test.expect(square(25), 625)
  end

  square(n).plus(3)
fullfills:
  test.expect(square-add3(10), 103)
  test.expect(square-add3(0), 3)
end

data List:
  | empty with
    length(self): 0 end,
  | link(first, rest :: List) with
    length(self): 1.plus(self.rest.length()) end
fulfills:
  test.expect(empty.length(), 0)
  test.expect(link(0, link(1, empty)).length() 2)
end

Proposed Semantics:

Functions will gain an optional block between "fulfills" and "end". Data definitions will also get a "fulfills" block. This can be any Pyret code, but presumably we will create some standard libraries that are intended to be used in this context.

Then, we will provide two ways of running Pyret code - in "test" mode, all code will run, but we will also run the fulfills blocks. We do this by way of the following desugaring:

A block of the following form:

...
fun f():
  ...
fulfills:
  f-foo()
end

...
fun g()
  ...
fulfills:
  g-foo()
end
...

data Foo:
  ...
fulfills:
  Foo-tests
end

Desugars to:

res =
  ...
  fun f():
    ...
  end

  ...
  fun g():
    ...
  end
  ...
  data Foo:
  ...
  end

%ff = [fun: f-foo() end, fun: g-foo() end, fun: Foo-tests end]
for list.each(ff from %ff):
  ff()
end
res

This will happen at every block level (so for nested functions as well as the top level). We do this so that when a fulfills block runs, everything that the function might need to be defined will already have been (as we will have gotten to the end of the scope, and the end of all outer scopes, before running the tests).