Sky Yoo

Programming languages research · UChicago coursework · Spring 2019

Assay

Constructing the input that breaks a higher-order program — a study of relatively complete counterexamples.

Assay — main view
ASSAY · MAIN VIEW

Most of the term went into building interpreters — a ladder of small languages, each one adding a feature and a proof obligation. It ends somewhere else: with a PLDI paper, read closely enough to hand-trace its semantics, and an implementation we spent three weeks failing to run.

The paper is Nguyen and Van Horn on counterexamples for higher-order programs — given a program that can crash, construct an input that crashes it, soundly and completely. The difficulty is that the input may be a function, and functions can’t be filtered the way integers can. Their method sidesteps the search entirely: step through evaluation of a symbolic program, accumulate first-order constraints in a heap as you go, and when the program errors, hand what you’ve collected to an SMT solver. The counterexample falls out of the model.

What the write-up became, in the end, was as much about the gap between a published method and a runnable artifact — and what you learn by writing to the person who built it.

The instrument

Assay — The observation that makes it tractable
PLATE I — THE OBSERVATION THAT MAKES IT TRACTABLE

The observation that makes it tractable

Searching the space of functions that might crash a program is hopeless — there are infinitely many. Nguyen and Van Horn's move is to note that if an unknown function can reach an error state at all, it can reach it by applying its input immediately, without loss of generality. What's left is first-order, and first-order constraints have solvers.

Assay — A heap that is also a path condition
PLATE II — A HEAP THAT IS ALSO A PATH CONDITION

A heap that is also a path condition

Each evaluation step allocates a fresh location and records what must be true of the value there. By the time the program errors, the heap holds enough first-order constraints to reconstruct the input that got it there — handed to Z3, and read back as a concrete counterexample.

Assay — Three weeks against a published artifact
PLATE III — THREE WEEKS AGAINST A PUBLISHED ARTIFACT

Three weeks against a published artifact

Every route to running their implementation was closed: the online evaluator's server was down, the VirtualBox image had expired, and building from source needed a Z3 version shipped only for Windows. Z3 worked fine in the terminal; their heap constructor still reported it missing. Their own CI was red at the time.

Assay — What the author told us
PLATE IV — WHAT THE AUTHOR TOLD US

What the author told us

Email correspondence with Phuc Nguyen turned up work that isn't in any paper — an extension to mutable state that finds counterexamples a pure language forbids, and his own assessment that they weren't interesting enough to pursue.

The movement

The engineering underneath

A term of building interpreters ends by reading someone else's, closely enough to trace it by hand and then try to run it.

Why higher-order inputs resist the usual trick

For a first-order program you can filter candidate inputs directly — an integer either satisfies the constraint or it doesn't. A function argument has no such test; you cannot enumerate the functions that might break f. The paper's answer is not to search that space at all, but to assume the worst-case shape of the culprit and let the constraints accumulate.

symbolic PCF
let f (g : int -> int) (n : int) : int =
  1 / (100 - (g n))
in (• f)

Tracing the heap by hand

We worked the paper's example through step by step rather than taking the result on faith. Each reduction allocates a location — L1 for the unknown, L2 and L3 for the arguments it applies, L4 and L5 for the intermediate integers — and every entry added is first-order. At the point of division the semantics branch non-deterministically, and refining L5 to zero selects the error path.

Where it bottoms out

The residue is an ordinary SMT problem. Z3 returns a model, the model is plugged back into the heap, and out falls a concrete function that crashes the program. This is the whole payoff of the reduction: the hard, higher-order search has become five lines a solver can read.

constraints handed to Z3
(declare-const L3 Int)
(declare-const L4 Int)
(declare-const L5 Int)
(assert (= L5 (- 100 L4)))
(assert (= 0 L5))

The artifact we could not run

Constructive engagement meant actually running their implementation, and we never managed it. Online evaluator down, VirtualBox image expired, and a source build that wanted a Windows-only Z3 build while our working Z3 went undetected. Three weeks, and the honest finding was about artifact rot rather than about the method — their continuous integration was failing at the same time we were.

Asking the author

We wrote to Phuc Nguyen about the build, and the correspondence outlasted the problem. He described the larger goal behind this line of work — making static analyzers less ad-hoc, since their soundness proofs tended to need fresh invention for every new language feature — and where counterexample generation had stalled. Extending it to mutable state does produce counterexamples, like the one below, that no pure function could give: g ignores its argument and returns zero the second time it's called. His verdict was that such examples are found but not interesting — not the kind of bug a programmer would care about.

from the correspondence
(define h
  (lambda f
    (if (= 0 (f 1)) 0 (/ 1 (f 1)))))

(define g
  (let ([counter 0])
    (lambda _
      (set! counter (+ 1 counter))
      (if (= counter 2) 0 1))))

(h g)
Specification
Paper
Nguyen & Van Horn, "Relatively Complete Counterexamples for Higher-Order Programs" (PLDI '15)
Subject
Symbolic execution · higher-order contract verification
Tools
Symbolic PCF · Racket · Z3
Correspondence
With the paper's first author, over the term
Origin
University of Chicago · CMSC 22100 (Programming Languages) · Spring 2019
Team
Partner project, completed with one collaborator