Picat code formatter

34 views
Skip to first unread message

Doug Edmunds

unread,
Dec 11, 2025, 8:58:04 PM12/11/25
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,
Dec 12, 2025, 8:56:22 AM12/12/25
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

Doug Edmunds

unread,
Dec 12, 2025, 11:02:15 PM12/12/25
to Picat
After many more attempts, ChatGPT could not format your DAY 4 code without errors (either moving your comments or failing indentation). 

GOOGLE LARGE LANGUAGE MODEL

I switched over to Google's large language model on their browser (it is not Gemini AI) and it balked at writing a reformating program, saying that proper formatting requires a lot of details, though it did offer to generate a basic formating tool..  It said a custom tool would require:
  1. A Picat Grammar Specification: You would need a formal grammar definition (e.g., in ANTLR or PLY format) to understand the language's structure. The Picat user guides provide high-level syntax descriptions, but not a machine-readable grammar file.
  2. A Python Parser: Using a parser generator like ANTLR or PLY would allow you to generate a parser from the grammar. This parser would convert the raw Picat code into an Abstract Syntax Tree (AST).
  3. Formatting Logic: You would traverse the AST and apply consistent style rules (indentation, spacing, line breaks, etc.) to generate the formatted output string.
  4. A Command-Line Interface (CLI): Libraries like Typer or argparse can be used to create a user-friendly command-line tool in Python.
Without a robust, grammar-based Picat parser written in Python (which doesn't currently exist as an off-the-shelf library), it is not possible to automatically reformat the provided code perfectly using Python.
However, I can manually format the code for readability and provide the result here, following typical Picat style conventions (indentation, line breaks after commas/commas before new lines in lists, consistent spacing).
Here is the provided Picat code, manually reformatted:

import util.
import aoc.

% Define a function/predicate 'n' (neighbor count)
% Counts '@' neighbors for cell (I, J) in board B of size N
n(B, N, I, J) =
    [1 : U in -1..1, V in -1..1,
        I + U >= 1, I + U <= N,
        J + V >= 1, J + V <= N,
        B[I + U, J + V] = '@',
        abs(U) + abs(V) != 0].sum.

% Define a function 'still'
% Checks if any '@' cells would change state based on neighbor count
still(B, N) =
    [[R : J in 1..N,
      R = cond((n(B, N, I, J) < 4, B[I, J] = '@'), 1, 0)].sum
     : I in 1..N].sum.

main([F]) =>
    B = rcm(F), % Read configuration matrix from file F
    pp(B),      % Pretty print board
    Init = [1 : Y in B, X in Y, X = '@'].sum, % Initial count of '@' cells
    N = B.length, % Board size

    while(still(B, N) > 0)
        B := [[R : J in 1..N,
               R = cond((n(B, N, I, J) < 4, B[I, J] = '@'), '.', B[I, J])]
              : I in 1..N],
        pp(B)
    end,

    Fin = [1 : Y in B, X in Y, X = '@'].sum, % Final count of '@' cells
    % The following lines are commented out in the original:
    % ,pp([[R:J in 1..N,R=cond((n(B,N,I,J)<4,B[I,J]='@'),1,0)]:I in 1..N])
    % ,pp([[R:J in 1..N,R=n(B,N,I,J)]:I in 1..N])
    p(Init - Fin). % Print the difference

CLAUDE AI

As a third attempt, I tried the Claude AI.  I asked it if it could reformat your code, which I provided to it by simple copy/paste.   
Amazingly, it produced a very good reformat of your code.  This is the link to the chat:

https://claude.ai/share/9c7a4b83-f416-44c7-9638-6d5a0042a304

SPOILER ALERT that link contains the solution to the puzzle (2025 AOC Day 4)

I did not ask Claude to generate a script that could be used for other picat code  But after it produced the reformatted code, I asked it how to input the puzzle date into your program, which it provided.   

 I asked Claude  to explain how your code worked (derived the solution), and it explained the algorithm and stepped through the process. 

I did not ask it to create a tool for reformating 

I suggest to anyone interested in which AI to look to for guidance with learning Picat to check out this chat with CLAUDE.  

Cristian Grozea

unread,
Dec 13, 2025, 3:34:40 AM12/13/25
to Doug Edmunds, Picat
Hi,

I fully agree, it would be very helpful to have picat syntax formal specs, with this many useful tools could be built. Here my attempt at porting the grammar given in the Picat user guide:
Based on it I wrotr also a simple syntax checker.
It needs more work, as it doesn't fully align with the Picat parser - it is usually more permissive. It is also too slow.

best regards,
CG

--
You received this message because you are subscribed to a topic in the Google Groups "Picat" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/picat-lang/RIzPmOpYu6Q/unsubscribe.
To unsubscribe from this group and all its topics, send an email to picat-lang+...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/picat-lang/7d97ebfd-a2c7-46de-9600-8f7d4c286ff4n%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages