UnifyWeaver

Chapter 2: Architecture Overview

The Transpilation Pipeline

The Prolog target follows a well-defined pipeline that transforms your source code into executable Prolog scripts. Understanding this architecture helps you:

Core Components

The Prolog target consists of three main modules:

1. prolog_target.pl - Main Orchestrator

Location: src/unifyweaver/targets/prolog_target.pl

Responsibilities:

Key Predicates:

% Main entry point
generate_prolog_script(+UserPredicates, +Options, -ScriptCode)

% Dependency analysis
analyze_dependencies(+Predicates, -Dependencies)

% Output handling
write_prolog_script(+ScriptCode, +OutputPath, +Options)

% Compilation (with error checking)
compile_script(+Dialect, +ScriptPath)
compile_script_safe(+Dialect, +ScriptPath, +Options)

2. prolog_dialects.pl - Dialect Abstraction

Location: src/unifyweaver/targets/prolog_dialects.pl

Responsibilities:

Key Predicates:

% Dialect support
supported_dialect(+Dialect)
dialect_capabilities(+Dialect, -Capabilities)

% Code generation
dialect_shebang(+Dialect, -ShebangLine)
dialect_header(+Dialect, +Options, -HeaderCode)
dialect_imports(+Dialect, +Dependencies, -ImportCode)
dialect_initialization(+Dialect, +Goal, +Options, -InitCode)

% Compilation
dialect_compile_command(+Dialect, +ScriptPath, -Command)

% Validation
validate_for_dialect(+Dialect, +Predicates, -Issues)

3. prolog_service_target.pl - Service Integration

Location: src/unifyweaver/targets/prolog_service_target.pl

Responsibilities:

The Generation Pipeline

Let’s walk through what happens when you call generate_prolog_script/3:

generate_prolog_script([factorial/2],
                      [dialect(gnu), compile(true), entry_point(test_factorial)],
                      ScriptCode)

Step 0: Dialect Selection and Validation

% Extract dialect from options (default: swi)
option(dialect(Dialect), Options, swi),

% Validate it's supported
supported_dialect(Dialect),

% Check code compatibility
validate_for_dialect(Dialect, UserPredicates, Issues)

What happens: Ensures the requested dialect exists and that your code is compatible with it.

Step 1: Dependency Analysis

analyze_dependencies(UserPredicates, Dependencies)

What happens: Scans your predicates to find:

Example output:

Dependencies = [
    module(unifyweaver(core/partitioner)),
    ensure_loaded(unifyweaver(core/partitioners/fixed_size)),
    plugin_registration(partitioner, fixed_size, fixed_size_partitioner)
]

Step 2: Shebang Generation

dialect_shebang(Dialect, ShebangCode)

What happens: Creates the executable shebang line.

SWI-Prolog:

#!/usr/bin/env swipl

GNU Prolog:

#!/usr/bin/env gprolog --consult-file

Step 3: Header Generation

dialect_header(Dialect, EnhancedOptions, HeaderCode)

What happens: Creates metadata comments about the script.

Example output:

% Generated by UnifyWeaver v0.1
% Target: Prolog (GNU Prolog)
% Compilation: interpreted
% Generated: 2025-11-17 22:13:14
% Predicates: 2

Step 4: Import Generation

dialect_imports(Dialect, Dependencies, ImportsCode)

What happens: Converts dependency list into dialect-specific import statements.

SWI-Prolog example:

:- use_module(unifyweaver(core/partitioner)).
:- ensure_loaded(unifyweaver(core/partitioners/fixed_size)).

GNU Prolog: May use different syntax or skip unsupported features.

Step 5: User Code Transpilation

generate_user_code(UserPredicates, Options, UserCode)

What happens: Extracts and formats your actual predicate definitions.

Input: [factorial/2]

Output:

factorial(0, 1) :-
    !.

factorial(A, B) :-
    A>0,
    C is A+ -1,
    factorial(C, D),
    B is A*D.

Step 6: Entry Point Generation

generate_main_predicate(Options, MainCode)

What happens: Creates the main/0 predicate that serves as the script’s entry point.

Output:

main :-
    test_factorial,
    halt(0).

main :-
    format(user_error, 'Error: Execution failed~n', []),
    halt(1).

Step 7: Initialization Code

dialect_initialization(Dialect, EntryGoal, Options, InitCode)

What happens: Generates dialect-specific initialization directives.

GNU Prolog (compiled):

% Entry point (for compiled binary)
:- initialization(test_factorial).

GNU Prolog (interpreted):

% Entry point (called on load)
:- test_factorial.

SWI-Prolog:

:- initialization(test_factorial, main).

Step 8: Assembly

atomic_list_concat([
    ShebangCode,
    HeaderCode,
    ImportsCode,
    UserCode,
    '\n% === Entry Point ===',
    MainCode,
    InitCode
], '\n\n', ScriptCode)

What happens: Combines all pieces into the final script with proper spacing.

Writing and Compilation

After generation, the script goes through output processing:

write_prolog_script(ScriptCode, OutputPath, Options)

This predicate:

  1. Writes the file:
    open(OutputPath, write, Stream, [encoding(utf8)]),
    write(Stream, ScriptCode),
    close(Stream)
    
  2. Makes it executable:
    shell('chmod +x OutputPath')
    
  3. Compiles if requested:
    (   option(compile(true), Options)
    ->  compile_script_safe(Dialect, OutputPath, Options)
    ;   true
    )
    

Error Handling and Fallback

The compilation step uses a safe wrapper:

compile_script_safe(Dialect, ScriptPath, Options) :-
    catch(
        compile_script(Dialect, ScriptPath),
        error(compilation_failed(FailedDialect, ExitCode), Context),
        handle_compilation_failure(...)
    )

Behavior:

Module Structure

src/unifyweaver/targets/
│
├── prolog_target.pl           # Main orchestrator
│   ├── generate_prolog_script/3
│   ├── analyze_dependencies/2
│   ├── write_prolog_script/3
│   └── compile_script_safe/3
│
├── prolog_dialects.pl         # Dialect abstraction
│   ├── supported_dialect/1
│   ├── dialect_*/* (code generation)
│   └── validate_for_dialect/3
│
└── prolog_service_target.pl   # Service integration
    └── (service-specific logic)

Data Flow Diagram

User Predicates ──┐
                  │
Options ──────────┼──► generate_prolog_script/3
                  │            │
                  │            ├─► analyze_dependencies/2
                  │            │        └─► Dependencies
                  │            │
                  │            ├─► dialect_shebang/2
                  │            ├─► dialect_header/3
                  │            ├─► dialect_imports/3
                  │            ├─► generate_user_code/3
                  │            ├─► generate_main_predicate/2
                  │            └─► dialect_initialization/4
                  │                     │
                  └─────────────────────┘
                                        ▼
                                  ScriptCode (atom)
                                        │
                                        ▼
                          write_prolog_script/3
                                        │
                                        ├─► Write file
                                        ├─► chmod +x
                                        └─► compile_script_safe/3
                                                 │
                                                 └─► gplc (if compile=true)

Key Design Principles

1. Separation of Concerns

2. Extensibility

3. Robustness

4. Testability

What’s Next?

In Chapter 3, we’ll explore Prolog dialects in detail, understanding the capabilities and constraints of SWI-Prolog and GNU Prolog, and when to use each.


Key Takeaways:


Previous: Chapter 1: Introduction to the Prolog Target 📖 Book 11: Prolog Target Next: Chapter 3: Understanding Prolog Dialects →