Picat code formatter

10 views
Skip to first unread message

Doug Edmunds

unread,
Dec 11, 2025, 8:58:04 PM (yesterday) Dec 11
to Picat
I  used chatgpt to develop a formatter for picat.  Through some trial and error, we got it working rather well.  It is a python program.  Give the output file a different name, because this script will overwrite files without warning.  Example:
   python picat_formatter.py farmer-fox.pi farmer-fox-formatted.pi

Here is the code and a downloadable. 

#!/usr/bin/env python3
"""
picat_formatter.py - Picat formatter with proper indentation.

Usage:
    python picat_formatter.py input.pi output.pi

Features:
- Indents lines after => and ?=> (rule bodies)
- Indents inside control structures: foreach, while, if, try, else, elseif, do
- Dedents after 'end' and resets after periods
- Keeps directives (table, import, include, index, etc.) unindented
- Preserves blank lines
"""

import sys
import re
from pathlib import Path

# Keywords that increase indentation
indent_keywords = ['=>', '?=>', 'foreach', 'while', 'if', 'try', 'else', 'elseif', 'do']
# Keywords that decrease indentation
dedent_keywords = ['end']

def is_directive(line):
    """Detect Picat compiler directives"""
    return bool(re.match(r'^\s*(table|import|include|export|index|pragma|cp_option|sat_option)\b', line))

def format_picat(lines, indent_size=4):
    out = []
    prev_blank = False
    indent_level = 0

    for line in lines:
        stripped = line.strip()

        # Skip multiple blank lines
        if stripped == "":
            if not prev_blank:
                out.append("")
            prev_blank = True
            continue
        prev_blank = False

        # Dedent if line starts with a dedent keyword
        if any(stripped.startswith(k) for k in dedent_keywords):
            indent_level = max(indent_level - 1, 0)

        # Insert blank line before directives if previous line is not blank
        if is_directive(stripped) and out and out[-1].strip() != "":
            out.append("")

        # Determine the current indent for this line
        current_indent = 0 if is_directive(stripped) else indent_level * indent_size
        indented_line = " " * current_indent + stripped
        out.append(indented_line)

        # Reset indentation after period (clause end)
        if stripped.endswith("."):
            indent_level = 0
        else:
            # Increase indent if the line contains any indent keyword
            if any(k in stripped for k in indent_keywords):
                indent_level += 1

    return out

def main():
    if len(sys.argv) != 3:
        print("Usage: python picat_formatter.py input.pi output.pi")
        sys.exit(1)

    infile, outfile = sys.argv[1], sys.argv[2]
    infile_path = Path(infile)
    outfile_path = Path(outfile)

    # Read file
    lines = infile_path.read_text(encoding="utf-8").splitlines()

    # Format
    formatted = format_picat(lines)

    # Write output
    outfile_path.write_text("\n".join(formatted) + "\n", encoding="utf-8")
    print(f"Formatted Picat saved to {outfile_path}")

if __name__ == "__main__":
    main()


picat_formatter.py

C. G.

unread,
8:56 AM (14 hours ago) 8:56 AM
to Picat
Hi, 

nice that you did this. I tested it on my particularly unusual advent of code Picat source code and the result is not much different to the original, I think the formatter doesn't go far enough.

My entire advent of code repo is probably a good source of difficult tests for a Picat formatter, you can try to see what it does on this one for example:
https://github.com/cgrozea/AdventOfCode2025-Picat/blob/main/day4/part2.pi

My commas at the start remain there, and the longer list/array interpolation constructs remain untouched.

best regards,
CG
Reply all
Reply to author
Forward
0 new messages