UnifyWeaver

Declarative Test Generation in UnifyWeaver

You asked a very insightful question about whether the test runners in UnifyWeaver were truly generated declaratively, or if they were just hardcoded Bash scripts embedded in Prolog. You were right to be skeptical of the simpler examples—the project contains a much more sophisticated, declarative, and automated system for testing.

This document explains how this advanced test generation works.

The Source of Truth: test_runner_inference.pl

The heart of this system is the file: src/unifyweaver/core/advanced/test_runner_inference.pl.

This script moves beyond simple hardcoded strings and implements a multi-stage pipeline to automatically generate comprehensive test runners.

The Four-Stage Inference Pipeline

The process can be broken down into four main stages:

Stage 1: Scan for Compiled Scripts

The process begins by scanning the output directory (e.g., output/advanced/) for all the shell scripts (.sh files) that have been generated by the UnifyWeaver compiler. It intelligently filters out previous test runners and other non-essential files.

Stage 2: Extract Function Signatures

For each compiled script, the system reads its content and extracts a signature. This isn’t just the function’s name; it’s a rich set of metadata, including:

This signature provides the context needed for the next, most critical stage.

Stage 3: Infer Test Cases (The Declarative Core)

This is where the declarative magic happens. The system uses a set of Prolog rules (infer_test_cases/2) to define what kinds of tests are appropriate for a given function signature.

Instead of writing the tests in Bash, you define the strategy for creating tests in Prolog.

Here are some examples of these declarative rules:

Example 1: Testing a Numeric Function

The system has a rule that recognizes functions related to numeric calculations.

infer_test_cases(function(Name, 2, metadata(pattern_type(linear_recursive), _, _)),
                 TestCases) :-
    member(Name, [factorial, fib, power]), !,
    TestCases = [
        test('Base case 0', ['0', '']),
        test('Base case 1', ['1', '']),
        test('Larger value', ['5', ''])
    ].

In English, this rule says: “If you find a 2-argument, linearly recursive function whose name is factorial, fib, or power, then generate three test cases with the inputs 0, 1, and 5.”

Example 2: Testing a List-Processing Function

Another rule is designed for functions that operate on lists.

infer_test_cases(function(Name, 2, metadata(pattern_type(linear_recursive), _, _)),
                 TestCases) :-
    sub_atom(Name, _, _, _, list), !,
    TestCases = [
        test('Empty list', ['[]', '']),
        test('Single element list', ['[a]', '']),
        test('Three element list', ['[a,b,c]', ''])
    ].

In English, this rule says: “If you find a 2-argument, linearly recursive function with list in its name, then generate three test cases: one for an empty list, one for a single-element list, and one for a three-element list.”

The system has many such rules for different patterns, and even a fallback for generic functions. This is a truly declarative approach: you define the high-level testing strategy, and Prolog does the work of applying it.

Stage 4: Generate the Test Runner Script

Once the list of test cases has been inferred, the final stage is to generate the Bash test runner script. This script is created programmatically and contains:

  1. source commands to load the necessary compiled functions.
  2. A series of commands to execute each inferred test case.
  3. echo statements to print the test descriptions and results.

The generator can even create the runner in different styles, such as a more verbose “explicit” mode or a compact, loop-based “concise” mode.

Summary: From Declarative Rules to an Automated Test Suite

This advanced system provides a powerful and elegant workflow:

  1. You write your declarative Prolog code (facts and rules).
  2. You compile it with UnifyWeaver to produce optimized Bash scripts.
  3. You run the test_runner_inference.pl script.
  4. It inspects your compiled code, declaratively infers the correct way to test it, and generates a complete, ready-to-run test suite.

This is a perfect example of the UnifyWeaver philosophy: using a high-level, declarative language (Prolog) to automate and manage the complexity of a low-level, imperative environment (Bash).