Alcides Fonseca.AI & Programming Languages researcher
40.197958, -8.408312

Making intelligent software
more reliable.

I work on the intersection of AI, Software Engineering and Programming Languages. Currently, I am applying Formal Methods to Agents, towards AI Safety.

Program synthesisLiquid typesReliable AIGuardrailsEvals

Posts tagged Program Synthesis

Clear filter

A review of Djinnlang

As I'm losing my faith in organized peer review, I've decided to take matters in my own hands and I'm doing distributed, post-publication peer-review.

Today's paper is Djinnlang: Higher-Level Programming by Unambiguous Specification with an LLM in the Compiler by Simon Henniger, Stephen Chong and Nada Amin.

In a time that people are relying on LLM agents to implement and verify their code, it is natural that people think of implementation source code as the new assembly: it will never be read, just produced by the new compilers, LLMs.

Djinn takes this approach: users only write specifications, and the compiler will write the source code and verify it. This part is not news.

The part that I found interesting that hadn't occurred to me is the unambiguity property of the specification (not the code's). If a specification allows for two implementations that return different values, the specification is wrong. Of course, if two implementations (say quick and merge sort) yield the same results, then the specification is correct and the compiler will produce a working implementation and verify it. If it can, because LLMs produce no guarantees, unlike classical compilers.

The paper then goes on to separate functions, procedures and relations, similarly to how Lean has partial, liquid types have measures and handle monads. These are details that are interesting only to those implementing a similar system, but not crucial to the main idea.

Overall, I think the idea of functional determinism is interesting for a specification. We actually need more techniques to validate the consistency of specifications, since that's how a lot of programming might be done in the near future.

But as someone who has been implementing a language that allows users to only write specifications, I'm not sure we only care about functional correctness, and that mainstream developers will write specifications as opposed to natural language. Despite my language supporting expressing logic-based requirements, examples, tests, property-based testing, and optimization metrics, I'm more and more convinced no one will read them. In the whole Requirements-Archicture-Design-Implementation-Testing/Verification-Validation, I think humans will stay at Requirements and Validation level, and everything else will be automated.

Overall impression: excellent idea of making specification unambiguous as a requirement. I would probably reduce the paper to two pages (intro and Figure 1 focused on the max_is_unambiguous). I don't see much value for the overall audience in the rest of the paper. It can be a technical annex or documentation in the repo for those of us implementing these approaches. Shorter and more focused papers are better and take nothing away from the core idea.

How to express intent in aeon

I started developing Aeon back in 2018 as a playground for program synthesis. Holes (?x) are valid expression in aeon (def f (i:Int) : Int := i * 2 + ?x compiles successfully and, when ?x is evaluated at runtime, it opens a repl asking the user for the right expression). However, the main advantage of holes is when you run the aeon compiler with "--synthesizer", it tries to find a suitable expression for the holes in your program. This blog post covers the different methods you have to describe what you want a function to do. We've decided on the function-level for the specification as it is the most normal public (and private) API of your code.

Basic Types

Aeon is a (very strongly) typed language, so you always need to write the types of top-level functions.1 As such, all proposed candidate expressions to replace ?hole need to type-check. So writing Int as the return type already specifies something about the missing code.

def double (x:Int) : Int := ?hole

Synthesis menu in VSCode for this simple hole

Liquid Types

Aeon supports liquid types by refining a basic type with a predicate. In the case below we refine the returning type of the function Int with the predicate that it needs to be equal to twice the value of x. Because Liquid Types can refer to other values in the program (such as function arguments), they are (value-)dependent types, but following a different discipline than the (full) dependent types you find in Lean, Rocq or Idris.

def double (x:Int) : {r:Int | r == x + x} := ?hole

So far, this definition covers both Refinement Types and Liquid Types. The main distinction of the two is that the refinements in Liquid Types need to be drawn from a decidable logic, so an SMT solver can give you a definitive answer of whether it type checks or not.

Liquid types guide synthesis directly in approaches such as Synquid and Liquid Tree Automata. In Aeon, their logical predicates can also be translated into constraints for SMT solving with Z3, syntax-guided synthesis (SyGuS), polynomial synthesis, and OR-Tools’ CP-SAT solver. Liquid Types are also used in our own custom synthesis algorithms that combine evolutionary search and liquid types.

Natural language documentation

Aeon also supports AeonDoc, using the @doc decorator to annotate a natural-language description of the function. We can take this documentation as input to a synthesis method (currently, only LLM-based synthesizers support @doc). This documentation complements any type that constrains the hole already.

@doc("Doubles the input.")
def double (x:Int) : Int := ?hole

Examples or test cases.

Aeon supports the decorator @example that takes a boolean predicate about the execution of the final synthesized function.

@example(double 3 = 6)
@example(double 5 = 10)
def double (x:Int) : Int := ?hole

Input–output examples guide many synthesis approaches: automata-based methods such as AFTA use them to prune spaces of candidate programs; top-down methods such as Myth or Decision Trees propagate examples into smaller synthesis subproblems; and bottom-up methods such as BUSTLE compose programs from smaller expressions evaluated on those examples. Examples also define fitness functions for genetic programming and provide training data for decision-tree learning, which learns conditional rules mapping inputs to outputs.

Decision Trees and Genetic Programming, coming from the Machine Learning backgrounds, assume that some examples might be wrong so they are more permissive. Other methods typically search for a solution that covers all examples. In this case, only noise-supporting methods can handle contradictory examples.

Because writing examples can be tedious, you can use CSV data directly or through an auxiliary file (@csv_data("1.0,2.0\n3.0,6.0\n4.0,8.0") or @csv_file("double.csv").

Optimization goals

@minimize_int(abs $ (double 4) - (4 * 2))
@minimize_int(abs $ (double 5) - (5 * 2))
def double (x:Int) : {r:Int | r = x + x} := ?hole

Aeon supports multiple maximize and minimize decorators to express optimization goals. We might know a given ideal target for the minimization criteria or not (for open problems). This example is the former, where you cannot have a value lower than 0. However, if you were to maximize the return value of a function that returns prime numbers, you would have no target value.

When multiple objectives are set, we consider this a multi-objective optimization problem and each algorithm typically returns the Pareto set, containing solutions that no other candidate is better in all axis. Genetic Programming and OR-Tools solvers have these features built-in. Recent Genetic Programming approaches have recommended Lexicase-selection instead of a Pareto tournament, but my own experience does not confirm this recommendation. If you are going to evaluate all objectives, then lexicase selection might make sense, but because tournaments only evaluate one objective for few good candidates, it ends up being more computational cheaper, and more time is left for exploration.

A big advantage of examples and optimization constraints over liquid types is that you can write any aeon expression that you want (such as multiplication of variables i*j). Because Liquid Types are limited to decidable theories, you cannot write2 this multiplication and other types of expressions and you are limited to linear integer arithmetic, arrays, uninterpreted functions and other SMT theories.

Property-based Tests

Property-based testing is the idea that instead of writing concrete input and output examples, you write properties that relate them, and have random inputs generated to validate those properties. For me, it is very similar, if not the same, as Metamorphic testing.

def double (x:Int) : Int := ?hole

@property(100)
def prop_double_is_bigger (x:Int | x > 0) : Bool := double x > x

In this example, the body of the double function will have to work with 100 randomly-generated inputs and return a value that complies with the state property every single time.

Results

You can either run aeon on the command line, selecting the synthesizer method with -s (python -m aeon multi_objective.ae -s gp --budget 10), or use an LSP editor to ask the compiler to synthesize using a particular method. If you are using VSCode, you have an extra interface to explore program synthesis.

VSCode UI for synthesizer programs

The first specification yielded def double (x:Int) : Int := 1, which matches exactly what we asked it to return: an Integer. When using the other specifications we got a variety of solutions: x+x, 2*x and x+x+0. But this is with a very simple specification that I would assume any synthesizer should be able to generate. Larger examples are left to the reader (or to the next blog post).

Non-functional requirements

All of the intent that we have expressed for far was related to functional correctness. But software developers know that non-functional properties can be equally important in some projects. As such, we are adding support for different forms of expressing quality properties. In particular, we already support @minimize_cputime(expression) and @minimize_energy(expression) for minimizing execution time and energy consumption respectively. Despite this support, the example above is too simple and trivial that the measurement overhead is many times that of the execution. I would only recommend using these advanced decorators for larger programs.

And aeon also supports other properties, you just need to write the code that translates the candidate function into a measure of that quality, using the example or optimization decorators.

Happy synthesizing!


  1. This restriction comes from Liquid Types. ↩

  2. Actually you can, but you will get a warning. This is an UX improvement we are making based on our paper on challenges of Liquid Types adoption. ↩