This is the documentation for the Synthesizer.jl program synthesis system.

The goal of this package is to provide a set of interfaces for probabilistic program synthesis via sketching. The hope is to faciliate research into probabilistic program synthesis, as well as provide a pipeline to optimize synthesized programs as IR fragments, before compilation into a method body in Julia.


The idea behind sketching is to provide a high-level skeleton of the program (or function) you want to synthesize. This high-level skeleton contains a number of "holes" which the synthesizer should fill with an expression or function.

Let's say we wanted to synthesize a sorting program.

function sort!(x::Array{T}) where T <: Number
    while !check(x)
        x = hole(array_operation, x)
    end
    return x
end

Here, I've provided the high-level structure of the function (e.g. control flow) but I've also inserted a hole, and I'm telling the synthesizer what sort of hole it should be (an array_operation function, which acts on x). array_operation is defined by a miniature DSL.

@lang (array_operation) _1 expr = begin
    0.20 => swap!(_1)
    0.20 => reorder(_1)
    0.20 => swap_head!(_1)
    0.20 => swap_tail!(_1)
    0.20 => reverse(_1)
end

This DSL is probabilistic - the numbers on the left-hand side correspond to probability of selection by the synthesizer. Furthermore, we've specified that holes of array_operation take in a single argument (here, by specifying _1 before defining the expr of type array_operation).

In our high-level program, we've cheated a bit by providing a way for the program to determine if it's sorted the list correctly. The function check checks if the array satisfies the sorted condition.

function check(x::Array{T}) where T <: Number
    length(x) == 1 && return true
    i = x[1]
    for j in x[2 : end]
        if !(i < j)
            return false
        end
        i = j
    end
    return true
end

To summarize: in Synthesizer.jl, the user writes small probabilistic DSLs (specifically, probabilistic context-free grammars) which can then be used inside holes in other functions. The process of synthesis is expressed using a universal trace-based probabilistic programming system.


The core of the engine is a multi-threaded rejection sampler.

function synthesize(sel::Array{K}, fn::Function, pair::T; iters = 50) where {K <: Jaynes.ConstrainedSelection, T <: Tuple}
    in, out = pair
    success = Jaynes.CallSite[]
    Threads.@threads for s in sel
        for i in iters
            ret, cl, w = generate(s, fn, in)
            out == ret && push!(success, cl)
        end
    end
    return success
end

function synthesize(fn::Function, pairs::Array{T}; iters = 50) where T <: Tuple
    local cls
    constraints = [selection()]
    for p in pairs
        cls = synthesize(constraints, fn, p; iters = iters)
        cls == nothing && return cls
        constraints = map(cls) do cl
            get_selection(cl)
        end
    end
    return cls
end

Here, synthesize requires that the user pass in a function with holes, as well as pairs of (input, output) tuples. generate generates a set of possible solutions. This version of synthesize will return an Array of successful traces (if the search succeeds). The CallSite representation is a trace of the original function, as well as a recording of the choices made by the PCFG.

In future versions, Synthesizer.jl will support the ability to compile these traces into IR and generate a new (optimized) method body for the synthesized function.