definition theorem example program

Two program fragments and are contextually equivalent (Morris), written , if no complete program can tell them apart:

where ranges over all well-typed contexts — programs with a hole — and is the chosen observation (termination, or termination with a value of ground type). It is the natural notion of “these two do the same thing” for a programming language, and it has two defining properties: it is a Congruence (if then , since contexts compose), and it is adequate (equivalent closed programs have the same observable result). In fact it is the largest adequate congruence. Everything finer — syntactic equality, -equality, equality by a set of rewrite rules — is contained in it; everything coarser, such as “agrees on a test suite”, is not a congruence.

Sources: Morris, Lambda-calculus models of programming languages, PhD thesis, MIT 1968; Milner, Fully abstract models of typed λ-calculi, Theor. Comput. Sci. 4 (1977); Plotkin, LCF considered as a programming language, Theor. Comput. Sci. 5 (1977) (the full abstraction problem for PCF); Pitts, Operational semantics and program equivalence, in Applied Semantics (LNCS 2395, 2002); Abramsky, Jagadeesan & Malacaria, Full abstraction for PCF, and Hyland & Ong, On full abstraction for PCF, Inform. and Comput. 163 (2000) (game semantics). Proof methods: Logical Relations, Bisimulation.

Why it is hard to prove directly

The quantifier over all contexts is the problem: a context can do anything the language allows, including things the programmer never intended. Two functions that agree on every input may still be distinguishable if the language lets a context observe sharing, timing of effects, exceptions, or — as in the Julia tab — the order in which equal elements are returned. Proving therefore goes through a proxy that is a congruence by construction:

methodidea
Logical Relationsa type-indexed relation that respects every construct; the fundamental lemma gives congruence
applicative / environmental Bisimulationa coinductive relation on closed terms, shown to be a congruence (Howe’s method)
fully abstract denotational modela semantics in which ; equality is then computed in the model

Finding a fully abstract model for PCF was open for two decades; game semantics solved it (2000). Categorically, a fully abstract model is a quotient of the syntactic category by contextual equivalence that is also given by some independent, compositional construction.

The observation is a parameter

Changing what counts as observable changes the relation: if running time or the number of allocations is observable, far fewer programs are equivalent. An equivalence “modulo timing” is contextual equivalence for a coarser observation. And across two languages the definition does not even type-check — would have to be a context of which language? A cross-language relation (a multi-language semantics, or a translation into a common core) is needed before the question can be asked.

Refinement, the one-sided version

Replacing by gives contextual refinement : every observation can make, can make too (or the converse, depending on convention — “fewer behaviours”, “more defined”). Refinement is the right notion for implementing a specification, for nondeterministic code, and for moving to a language with a stricter discipline. It is a preorder, not an equivalence, and the order of a cartesian bicategory of relations is its abstract form.

Sophia

Sophia’s observational equivalence level is contextual equivalence modulo a stated set of ignored observations (alloc, timing, fp_assoc, …), and its REFINES edge is contextual refinement. The design notes’ cautionary example — two sorting functions that agree on every test but differ on stability — is the Julia tab below: tested equivalence is not a congruence, so it may not license substitution. See Equivalence and Witnesses.

Docs: plain Julia — Catlab has no dedicated API for this; related: Catlab v0.16 docs · GATlab standard library

using Random; Random.seed!(3)
# Two sorting functions by key: insertion sort (stable) and selection sort with swaps (unstable).
function stable_sort(xs; by = identity)
    out = similar(xs, 0)
    for x in xs
        i = findfirst(y -> by(y) > by(x), out)
        insert!(out, i === nothing ? length(out) + 1 : i, x)
    end
    out
end
function unstable_sort(xs; by = identity)
    a = copy(xs)
    for i in eachindex(a)
        j = i - 1 + argmin([by(y) for y in a[i:end]])
        a[i], a[j] = a[j], a[i]                       # the swap can jump equal keys past each other
    end
    a
end
# A test suite over integers cannot tell them apart …
tests = [rand(-9:9, rand(0:8)) for _ in 1:500]
all(stable_sort(t) == unstable_sort(t) for t in tests)                 # true
# … but a context that sorts records by key and then observes their order does:
C(sortfn) = [r[2] for r in sortfn([(2, :a), (1, :b), (2, :c), (1, :d)]; by = first)]
C(stable_sort)                                                         # [:b, :d, :a, :c]
C(unstable_sort)                                                       # [:b, :d, :c, :a]
C(stable_sort) == C(unstable_sort)                                     # false: not contextually equivalent
import Mathlib
-- Two sorts by key: Mathlib's insertion sort (stable), and the same sort on the reversed list,
-- which puts records with equal keys in reverse order (unstable).
def keyLE (a b : ℕ × Char) : Prop := a.1 ≤ b.1
instance : DecidableRel keyLE := fun a b => inferInstanceAs (Decidable (a.1 ≤ b.1))
def stab (l : List (ℕ × Char)) := l.insertionSort keyLE
def unst (l : List (ℕ × Char)) := l.reverse.insertionSort keyLE
 
def recs : List (ℕ × Char) := [(2, 'a'), (1, 'b'), (2, 'c'), (1, 'd')]
-- a context that only observes the keys cannot distinguish them …
example : (stab recs).map Prod.fst = (unst recs).map Prod.fst := by decide
-- … the context that observes the whole records can
example : (stab recs).map Prod.snd = ['b', 'd', 'a', 'c'] := by decide
example : (unst recs).map Prod.snd = ['d', 'b', 'c', 'a'] := by decide