Hi, I worte and would like to share an authorative Prolog formatter. Had a nicer message, but seems I can only write 2 lines of text as a newly registered. Link to repo: github:exlee/prolog-fmt. If there’s interest I’ll share more (and maybe 2 line limits will be gone, too) ![]()
Your issue should be resolved now.
Please let me know if you experience any further problems with the forum.
Full announcement! ![]()
So, I’m sharing something that I’ve been pondering for long time (and every time when missing semicolon ate my soul) - a Prolog formatter[1]. It’s an early stage because I pulled the trigger very recently, but spent ~20h hours at it.
[repo link above]
I have right now corpus of around 40 cases and I’m still checking random samples[2] and adding various rules. Some of the formatting samples I find get into the corpus.
Some - arguably - I format better… ![]()
If anyone is interested feel free to check it out. In case you disagree with formatting, start an issue or submit corpus entry.
Few pitchable features:
- since it is authorative, it will vomit letters on the screen if you forget a comma or a dot
(making mistakes easier to spot):remove │ this dot│ % after (easy to spot an issue): ││ first :- ││ first :- one, ││ one, two, ││ two, three.◀──┘│ three second :- alpha, │ second :- │ beta. alpha, │ beta. │ - it formats brackets[3] depending on newline position
(e.g. inserting newline after 2nd item in a list will keep 2 items per line) - it’s fast; that’s Rust, so there’s almost non-existent startup, in fact:
$ wc -l samples/chr/chr_translate_bootstrap.pl 2496 samples/chr/chr_translate_bootstrap.pl $ hyperfine "target/debug/prolog-fmt samples/chr/chr_translate_bootstrap.pl" Benchmark 1: target/debug/prolog-fmt samples/chr/chr_translate_bootstrap.pl Time (mean ± σ): 34.2 ms ± 0.7 ms [User: 33.0 ms, System: 2.7 ms] Range (min … max): 32.7 ms … 35.8 ms 78 runs
Hello,
Thank you for sharing your project.
One thing I am wondering about is: how do you deal with prolog inherent dynamicity given you implemented this in rust ?
For example, we can define new operators at runtime, meaning you actually have to interpret prolog code while parsing it.
I guess you chose to implement it in rust for speed (like the recent trend of ruff in python) but I feel like this could be a fundamental limitation ?
Maybe you could reuse a rust library from scryer to do the parsing ?
Another thing that can impact formating (well, this is subjective) is the formatting of meta-predicates and lambda. I often tend to put callable terms in their own line to emphasize what they are.
Sorry for commenting without testing your project, but it is difficult to test as I don’t have a working rust toolchain. maybe if you provided a precompiled binary ?
One thing I am wondering about is: how do you deal with prolog inherent dynamicity given you implemented this in rust ?
I pondered about it, and there are few angles:
- (harshest) sometimes I don’t - Prolog code can be made into pure magic - a tool cannot cater to every case, so sometimes it just won’t work, this is fine
- (for popular libraries) by incremental support, CHR and DCG is an example of such behavior: I just add support for CHR and DCG predicates separately. I also plan to have quasiquotes and everything I use but it’s not supported yet

- Use a fallback behavior (which works well!). For example if you define
==:==operator and I see:
I don’t consideryour_predicate(X) :- has_value(X), X ==:== Z, has_value(Z).==:==anything special. I don’t have rule for it. Thus I’m going to treatX ==:== Zas aClausetype and use only that level rules (often it’s “leave unchanged”)
The problem is often brought, but while I see theoretical problem I’m not sure if it has high impact on most of the code.
I guess you chose to implement it in rust for speed (like the recent trend of ruff in python) but I feel like this could be a fundamental limitation ?
I’ll put a joke that The Users (count: 1) influenced the decision ![]()
I worked with a lot of tooling, static analyzers etc., and self-analysis often puts more weights into solution than it solves the problem. Here I don’t care about atoms, heads, expansions etc. On the basic level I have: facts (no body), predicates (head + body, separated), and clauses (separated by ,,;). Thus:
9999999 XXXXX :- ?!?!?!?! ; !??!?!?!?.
Is completely valid predicate (even though it isn’t in Prolog). Exploration is descending, so first I’ll separate 9999999 XXXXX and then seei if I can reclassify Head, if not - it stays like it is.
Thus I don’t really need a flexibility (though Rust isn’t inherently limited when it comes to interaction, it’s limited by it’s own internal interaction) for solution to be working well.
Maybe you could reuse a rust library from scryer to do the parsing ?
Something I actively decided not to do. I’m not trying to evaluate code with all it’s effect, I’m trying to align it. Joking again: I don’t need to know physical qualities of the sausage to eat it.
Another thing that can impact formating (well, this is subjective) is the formatting of meta-predicates and lambda. I often tend to put callable terms in their own line to emphasize what they are.
Lambda will be a separate Clause type - I have it planned. Similarily I have ParenthesizedConditionClause that has syntax of ( ... ; ... ; ...) - it aligns (, ), ; and -> on same columns and indents inner clauses. It’s just a special rule for special types of clauses.
Sorry for commenting without testing your project, but it is difficult to test as I don’t have a working rust toolchain. maybe if you provided a precompiled binary ?
Please check in <12h from this post. I haven’t put a CI compilation because I wasn’t sure if there’s any interest in it. I’ll happily provide one.
P.S. In the meantime I recommend checking corpus entries. It has a simple syntax of:
% <<<
<PROLOG INPUT>
% >>>
<FORMATTED OUTPUT>
All the tests in corpus are passing, so it gives a good enough feel for what’s happening.
I’m using fedora 44
if you can make it compatible with it, I will try it and give some feedback.
I’m sorry, I forgot to put this in my task list and it flew out of my head. Working on this now! ![]()
Edit: It’s there - Release Nightly · exlee/prolog-fmt · GitHub
Oh, this is very interesting! I recently added code formatting to my Prolog LSP server - it would be interesting to see how they compare! It would be nice if they were somewhat in accord, too…you may find my test cases for formatting in that repo interesting to run your formatter on as well!
I might check and do some HTML comparison, would be fun (no promises though!)
I skimmed through the formatter and I have something to consider: I learnt that good formatter should always take stdin as an input: many editors allow to format the chunk, and for example with my formatter I’m actually hooking to NormalIdle event to Kakoune.
This means that formatter works pretty much live (thus the requirement of it being blazingly fast).
I “stole” that concept from Go and Zig formatters, which are unapologetically efficent in formatting code (and relieve developer from even thinking about it
).
Surely a good idea in general; in my case though, the formatter works via the LSP specification, which operates in a “batch” mode, passing a file path and optional range (plus the library I use to parse Prolog code needs to operate on a real file to maintain source positions as it as what you refer to as an “adjustive” formatter).
I made the diffs, report is here:
I used swipl lsp_server/app/formatter.pl INPUT.pl --output_to=TMP_FILE.pl for lsp_server output. Assuming I didn’t botch invocation I found some bugs ![]()
For example:
- sample 4 fails completely (main/1 returns false)
- sample 22 removes dots from facts
- sample 9 is quite nefarious because LSP formatter removes unbalanced code (so syntax error + formatting =
) - there are couple blank outputs, which I consider error.
Rest is matter of the opinion thus obviously I have my favorite ![]()
Edit: BTW this report is a good showcase of decisions made on prolog-fmt side.
I have to say that I really like the output of prolog-fmt ![]()
Yeah, the LSP formatter doesn’t offer edits for invalid syntax and is designed to be used in a different way than yours (because it’s part of a language server). I would also note that it looks up operator definitions dynamically, so your examples that show it failing on CHR is because your examples don’t load the library; does your formatter handle arbitrary operator definitions, or has it special-cased CHR?
Makes sense, however I’d argue that it shouldn’t be the case when formatting because:
- libraries might be implicitly loaded (cli, config file, that’s how I do it
) - contract requires user to be explicit or not use feature (i.e. no DCG formatting before DCG usage declaration)
Context analysis is understandable, but given my experience with various formatters and linters, the best are the ones that don’t do very deep analysis (like go fmt, Zig’s zls - my favorite in this space)
It has both:
hello <=> "Hello World"is an item, and is treated as such, no specializationhello,<=>,"Hello World"- when I discern operator, I can treat it as “Predicate”hello,CHR(<=>),"Hello World"- when I categorize operator as CHR “Predicate” becomes “CHR Predicate” and I might have new rules for it
On principle: everything is handled, the question is how deep it’s handled. Even if I don’t discern a specific operator I still have higher level rules to work on. For some operators I might not need to go very deep and just stop at a “Predicate” level.