A Prolog language implementation for .NET 10, written in C# 14.
The goal is a first-class Prolog experience on the .NET SDK, the way C# and F# have one: .dplproj projects, a plc compiler, dotnet prolog, dotnet new templates, and NativeAOT publishing.
Read the DotProlog documentation for tutorials, how-to guides, reference, and explanations of the design. New to Prolog — or to programming? Start with A Gentle Introduction to Prolog, a free beginner book in English and Russian whose examples all run on DotProlog.
Status: early, but usable. dotnet new prolog-console through dotnet publish -p:PublishAot=true
works today, and dotnet test discovers Prolog tests under Microsoft.Testing.Platform. .dplproj
predicate bodies compile to generated C# and ordinary CLR IL at build time; source consulted at run
time compiles to bytecode for an AOT-safe VM. The standalone plc compiler in this checkout emits
IL directly and can publish a native executable with NativeAOT. It can be packed and installed as
DotProlog.Compiler.Cli; this new tool has not yet been published to NuGet.org.
The packages have been on NuGet.org since 0.2.0 and the current release is 0.15.0: see
CHANGELOG.md and COMPATIBILITY.md.
% samples/HelloProlog/hello.pl
:- initialization(main).
main :-
greeting(Greeting),
write(Greeting),
nl.
greeting('Hello! World!').$ dotnet run --project src/DotProlog.Tool -- run samples/HelloProlog/hello.pl
Hello! World!Or, with just:
$ just helloAnalyze source without consulting it or executing directives:
$ dotnet run --project src/DotProlog.Tool -- lint --warnings-as-errors path/to/program.plThe stable linter diagnostics cover singleton variables, repeated underscore-prefixed singleton
markers, and double-quoted text read one way but used another — characters handed to a code
conversion, or to a grammar that compares character codes. An opt-in --profile covington adds
configurable source-layout checks. See
Source linting for rules and exit codes.
var engine = new PrologEngine();
engine.ConsultText("colour(red). colour(green). colour(blue).");
foreach (PrologSolution solution in engine.Query("colour(C)").Solutions())
{
Console.WriteLine(solution["C"]); // red, green, blue
}
bool ok = engine.Query("1 < 2").Prove();Answers are produced on demand and marshalled into plain .NET objects as they arrive, so they stay valid after the query has moved on — and an unbounded goal is fine as long as you stop taking:
var first = engine.Query("between(1, 1000000000, X)").Solutions().Take(4);Predicates can also be called directly with .NET values. This is the surface the generated .dplproj facades sit on:
var host = new PrologHost(engine.Machine);
PrologPredicate discount = host.Bind("discount", 3);
PrologValue[]? result = host.CallOnce(
discount, PrologInput.Float(100.0), PrologInput.Integer(10), PrologInput.Output);This works from F# and VB the same way. One engine runs one goal at a time and is not thread-safe.
samples/PricingRules is a .dplproj holding ordinary ISO Prolog plus a contract declaring its .NET
surface. samples/PricingConsole is a plain C# app that references it:
<ProjectReference Include="..\PricingRules\PricingRules.dplproj" />% pricing.dpli — modes and determinism live here, so pricing.pl stays portable
:- clr_module('Pricing').
:- clr_export(discount/3, det, [in(price, float), in(percent, integer), out(result, float)]).
:- clr_export(in_catalogue/1, semidet, [in(item, atom)]).
:- clr_export(bundle/2, nondet, [in(items, list(atom)), out(bundle, list(atom))]).IPricingModule pricing = PricingModule.Create();
pricing.Discount(100.0, 15); // 85
pricing.InCatalogue("widget"); // true
foreach (var b in pricing.Bundle(["widget", "gadget"])) { /* streamed */ }The facade is generated during the build, before the C# compiler runs. Nothing in the consuming code
mentions the engine, a goal, or a term — which is why the same reference works from F# and VB with no
extra work. That is checked rather than assumed: samples/PricingFSharp and
samples/PricingVisualBasic consume the same .dplproj, and the integration test builds and runs all
three.
The generated surface follows Microsoft's F# component design
guidelines
for "vanilla .NET" libraries: the TryGetValue pattern for semidet predicates with outputs, a named
record rather than a tuple for several outputs, collection interfaces rather than concrete types, null
checks at the boundary, and a CancellationToken on anything that streams. A predicate whose name
reads poorly in C# can be renamed in the contract, the equivalent of F#'s [<CompiledName>]:
:- clr_export(nrev/2, det, [in(l, list(atom)), out(r, list(atom))], 'NaiveReverse').$ dotnet run --project samples/PricingConsole
100 less 15% = 85
total 1200 is gold
widget in catalogue: True
bundles of [widget, gadget]:
[widget, gadget]
[widget]
[gadget]
[]The plc project emits an executable .NET assembly directly, without generating C# or invoking
Roslyn. Run it from this checkout with the .NET 10 SDK:
# Emit managed IL and an isolated publishing project.
dotnet run --project src/DotProlog.Compiler.Cli -- \
samples/HelloProlog/hello.pl --output artifacts/hello-il
dotnet artifacts/hello-il/PrologProgram.dll
# Or compile and publish native code in one command (host platform by default).
dotnet run --project src/DotProlog.Compiler.Cli -- \
samples/HelloProlog/hello.pl --output artifacts/hello-native --aotTo install plc from a locally built package, run these commands from the repository root:
dotnet pack src/DotProlog.Compiler.Cli -c Release -o artifacts/compiler
dotnet tool install DotProlog.Compiler.Cli --add-source artifacts/compiler \
--tool-path artifacts/compiler-tools --version 0.15.0
./artifacts/compiler-tools/plc --version
./artifacts/compiler-tools/plc samples/HelloProlog/hello.pl --output artifacts/installed-hello --aotUse the version produced by dotnet pack if you change the repository version. For a global
installation, replace --tool-path artifacts/compiler-tools with --global; the command is then
plc from any directory on your tool PATH. A project-local installation also works with
dotnet new tool-manifest followed by dotnet tool install --local and the same package, source,
and version arguments. The package includes all four DotProlog libraries needed by emitted programs;
the installed compiler needs .NET 10, and --aot also needs the SDK and native build tools.
The native executable is artifacts/hello-native/native/PrologProgram (PrologProgram.exe on
Windows). Publishing needs the .NET SDK and the platform's NativeAOT prerequisites. Running the
published executable needs neither an installed .NET runtime nor Prolog. Cross-OS publishing is
not supported by this command; use a build host for each target OS.
- Pass source files in load order. Applications start with their
:- initialization(...)goals. --mode modern|strict-isoand--flag double_quotes=codes|chars|atom|stringselect source semantics.--rid <RID>chooses the native target with--aot; it defaults to the host RID.--versionprints the compiler package version, including any prerelease suffix.--outputmust name a new directory. Existing outputs are never overwritten.- The managed output includes the required DotProlog DLLs and
PrologProgram.csproj. Its overriddenCoreCompiletarget supplies the already-emitted IL to the SDK. You can publish it later withdotnet publish artifacts/hello-il/PrologProgram.csproj -c Release -r <RID>. - Compiled predicates are static IL methods sharing the existing machine. Runtime
consult/1and assertions continue to use its bytecode path. The bundled standard library also retains its existing runtime initialization; this is not a bytecode-free runtime. - This compiler builds console applications from
.plfiles..dplproj,.dplifacades, and Prolog test projects continue to use the existing SDK backend. Portable PDBs are not emitted yet.
Compiler exit codes are 64 for usage errors, 65 for source diagnostics, 70 for I/O or host errors,
and 130 for cancellation or publishing timeout. Native publishing failures propagate dotnet's
exit code and retain the emitted managed artifacts. Generated applications return 0 on success,
1 when initialization fails, the requested halt/1 code, or 70 for an uncaught Prolog exception.
The native publishing timeout is 15 minutes; Ctrl+C terminates the child process tree.
| Path | What it is |
|---|---|
src/DotProlog.Syntax |
Lexer, ISO operator table, operator-precedence reader, diagnostics |
src/DotProlog.Runtime |
Tagged terms, heap, trail, choice points, bytecode VM, builtins |
src/DotProlog.Compiler |
Clause analysis, source linting, bytecode lowering, consult and embedding API |
src/DotProlog.CodeGen.CSharp |
.dpli contract reader, facade and entry-point generators, and the Prolog-to-C# predicate emitter |
src/DotProlog.CodeGen.IL |
Direct managed PE/IL emission and static program installation |
src/DotProlog.Compiler.Cli |
Standalone plc compiler and NativeAOT publishing driver |
src/DotProlog.Build.Tasks |
MSBuild task that runs the generator |
src/DotProlog.Sdk |
The DotProlog.Sdk MSBuild SDK package |
src/DotProlog.Templates |
dotnet new prolog-console, prolog-lib, and prolog-test |
src/DotProlog.Testing |
Microsoft.Testing.Platform host for Prolog tests |
src/DotProlog.Tool |
The dotnet prolog command |
tests/ |
Unit tests per component, plus end-to-end execution tests |
benchmarks/ |
BenchmarkDotNet suite for the reader, compiler, and engine |
docs/ |
Diátaxis documentation, built with MkDocs |
samples/HelloProlog |
The Hello World sample |
samples/PricingRules |
A .dplproj: Prolog rules plus their .dpli contract |
samples/PricingConsole, samples/PricingFSharp, samples/PricingVisualBasic |
C#, F#, and VB apps referencing it |
samples/GreetingApp |
A Prolog application built from a .dplproj |
samples/GreymereAdventure |
A complete fantasy text adventure written in Prolog |
samples/PricingTests |
Prolog tests in a .dplproj, run by DotProlog.Testing |
samples/TextGrammar |
A .dplproj whose DCGs walk double-quoted text as characters |
samples/NaturalLanguage |
Natural-language parsing and question answering over character lists |
samples/AotAcceptance |
The NativeAOT acceptance sample |
Documentation tasks require uv. The project pins Python 3.14 and the
complete documentation environment in uv.lock; run uv sync --locked --only-group docs once to
prepare it.
just build # build everything
just test # run every test project
just format # format all C# with CSharpier
just format-check # fail if anything is unformatted
just docs # build the MkDocs site with strict link validation
just docs-serve # preview the documentation with live reload
just check # format-check + docs + build + test
just run FILE.pl # consult and run a Prolog file
just bench '*' # run the benchmark suiteSee the justfile for the underlying dotnet and uv commands.
Build-time backends and runtime consultation share one reader, loader, and clause compiler:
SDK build-time Prolog : reader -> loader -> bytecode -> generated C# -> Roslyn -> IL -> JIT/NativeAOT
Standalone plc : reader -> loader -> bytecode -> direct IL emission -> JIT/NativeAOT
Runtime consult / assert : reader -> loader -> bytecode -> AOT-compatible bytecode VM
A .dplproj takes the first path: its predicates become direct-threaded C# blocks at build time, so
generated applications, facades, and test hosts neither embed nor consult their source. plc lowers
the same portable compiler model directly into IL blocks. Runtime consultation never emits CLR IL, so it stays valid inside a NativeAOT process — runtime-loaded predicates
execute as bytecode and are not turned into new machine code. Both drive the same heap, trail, and
choice-point state, so compiled and consulted predicates call each other freely.
That is verified, not assumed. samples/AotAcceptance publishes to a self-contained native executable with no managed assemblies beside it, then at run time consults a .pl file it has never seen, enumerates solutions, asserts and retracts clauses, and catches an ISO error — with zero trimming or AOT warnings in the build. CI runs it on Windows, Linux, and macOS:
$ DOTPROLOG_RUN_AOT_TESTS=1 dotnet test --project tests/IntegrationDynamic predicates use the logical update view, so a goal sees exactly the clauses that existed when it started:
:- dynamic p/1.
p(1).
p(2).
% [1,2] — the asserted clauses are not visible to the goal that asserted them
?- findall(X, (p(X), assertz(p(9))), L).The engine owns its control state: heap, trail, environment stack, choice-point stack, and argument registers are plain arrays, and Prolog calls are jumps inside a single dispatch loop. Prolog recursion depth therefore does not consume CLR stack, and failure is a return value rather than an exception. Last-call optimisation makes tail recursion run at constant stack depth.
A program runs in one of two modes, fixed when its engine is created:
| Mode | Surface | "abc" reads as |
A character is |
|---|---|---|---|
modern (default) |
ISO Parts 1–3 plus the documented extensions and a few documented modifications, SWI-Prolog-aligned | [a,b,c] |
a Unicode code point |
strict-iso |
Only the ISO/IEC 13211 Parts 1–3 surface | [97,98,99] |
a UTF-16 code unit |
?- "abc" = [L|Ls].
L = a, Ls = [b,c].ISO makes the initial double_quotes value implementation defined. chars is what Scryer, Trealla,
ichiban, Flowlog, and Trilog use, and it lets a grammar over text look like the text it parses. A
program written for code lists keeps working with one override: double_quotes=codes in the
DotPrologFlags project property, --flag double_quotes=codes on the command line, or
:- set_prolog_flag(double_quotes, codes). at the top of a file.
$ dotnet prolog run --mode strict-iso program.pl
$ dotnet prolog run --flag double_quotes=codes legacy.plA .dplproj selects its mode with <DotPrologLanguageMode>. See the
language guide for details.
| Area | Predicates |
|---|---|
| Terms | atoms, variables, unbounded integers, rationals, floats, strings, lists, double-quoted character lists, structures |
| Control | ,/2, ;/2, ->/2, *->/2, \+/1, !/0, call/1..8, once/1, repeat/0, ignore/1, not/1, true/0, fail/0 |
| Exceptions | throw/1, catch/3, with catchable ISO error/2 terms |
| All solutions | findall/3,4, bagof/3, setof/3, forall/2, call_nth/2, countall/2, aggregate_all/3,4 and aggregate/3,4 (count, bag, set, sum, max, min) |
| Database | assertz/1, asserta/1, retract/1, clause/2, retractall/1, abolish/1, :- dynamic |
| Ranges | between/3, with inf as an open upper bound |
| Loading | consult/1, ensure_loaded/1 at run time |
| Unification | =/2, \=/2 |
| Arithmetic | ISO-oriented evaluable functors over unbounded integers, rationals (1r3, rdiv/2), and floats; is/2, =:=/2, =\=/2, </2, >/2, =</2, >=/2 |
| Standard order | ==/2, \==/2, @</2, @>/2, @=</2, @>=/2, compare/3 |
| Term inspection | functor/3, arg/3, =../2, copy_term/2, term_variables/2, numbervars/3, variant/2, ?=/2, setarg/3, nb_setarg/3 |
| Type tests | var/1, nonvar/1, atom/1, number/1, integer/1, float/1, rational/1, string/1, atomic/1, compound/1, callable/1, is_list/1, ground/1 |
| Text | atom_length/2, atom_chars/2, atom_codes/2, number_chars/2, number_codes/2, char_code/2, atom_number/2, atom_concat/3, sub_atom/5, atomic_list_concat/2,3, upcase_atom/2, downcase_atom/2, char_type/2, code_type/2 |
| Strings | atom_string/2, string_chars/2, string_codes/2, string_concat/3, string_length/2, number_string/2, string_to_atom/2, term_string/2, sub_string/5, split_string/4, string_code/3, string_lower/2, string_upper/2 |
| Lists | length/2, append/3, member/2, memberchk/2, nth0/3,4, nth1/3,4, last/2, reverse/2, select/3, selectchk/3, subtract/3, intersection/3, union/3, delete/3, list_to_set/2, permutation/2, flatten/2, numlist/3, sum_list/2, max_list/2, min_list/2, max_member/2, min_member/2, pairs_keys_values/3, pairs_keys/2, pairs_values/2, transpose_pairs/2 |
| Higher order | maplist/2..8, foldl/4..6, include/3, exclude/3, partition/4 |
| Sorting | sort/2, sort/4, msort/2, keysort/2, predsort/3 |
| Ordered sets | list_to_ord_set/2, ord_empty/1, ord_memberchk/2, ord_subset/2, ord_disjoint/2, ord_union/2,3, ord_intersection/2,3, ord_subtract/3, ord_add_element/3, ord_del_element/3 |
| Assocs | AVL association lists: empty_assoc/1, put_assoc/4, get_assoc/3, list_to_assoc/2, ord_list_to_assoc/2, assoc_to_list/2, assoc_to_keys/2, assoc_to_values/2, min_assoc/3, max_assoc/3, del_assoc/4 |
| Validation | must_be/2, is_of_type/2, and the library(error) raisers from instantiation_error/1 to syntax_error/1 |
| Global variables | nb_setval/2, nb_getval/2, b_setval/2, b_getval/2, engine-scoped |
| Coroutining | freeze/2, frozen/2 |
| Cleanup | setup_call_cleanup/3, call_cleanup/2 |
| Integers | succ/2, plus/3 |
| Output | write/1,2, writeq/1,2, print/1,2, writeln/1, write_canonical/1,2, write_term/2,3, nl/0,1, format/1,2,3, tab/1,2, print_message/2 with message_hook/3 |
| Operators | op/3, current_op/3 |
| Grammars | -->/2 with {}/1, !, \+//1, ->//2, call//1, phrase//1, semicontexts and pushback lists; phrase/2, phrase/3; Name//Arity indicators |
| Streams | open/3,4 text and binary streams, close/1,2, configurable EOF actions, current_stream/1, stream_property/2, set_stream_position/2, current-stream selection, EOF inspection, flushing |
| Reading | term, character, character-code, and byte input/output; read_term_from_atom/3, term_to_atom/2, atom_to_term/3; char_conversion/2, current_char_conversion/2 |
| Modules | ISO interfaces and bodies with module/1, body/1, export/import/re-export, metapredicate/1, reflection, and Module:Goal; Quintus-style declarations in Modern mode |
| Directives | :- Goal, :- initialization(Goal), SWI's :- initialization(Goal, When) in Modern mode, halt/0, halt/1 |
Control constructs are compiled in place inside a clause body, so cut scopes the way ISO specifies: opaque in the condition of if-then-else, transparent in its branches, clause-scoped elsewhere. A bootstrap library written in Prolog makes the same constructs reachable when a goal is assembled at run time and passed to call/1.
sign(N, S) :- ( N < 0 -> S = negative ; N =:= 0 -> S = zero ; S = positive ).
safe_divide(X, Y, R) :-
catch(R is X / Y, error(evaluation_error(zero_divisor), _), R = undefined).
item(1).
item(2).
item(3).
squares(L) :- findall(S, (item(N), S is N * N), L). % L = [1,4,9]Every error the engine raises is a catchable error(Formal, Context) term, so existence_error, type_error, instantiation_error, and evaluation_error can all be handled in Prolog rather than aborting the run.
halt/1 validates its status before terminating: variables and non-integers remain catchable input
errors instead of being converted to exit code zero.
compare/3 likewise distinguishes an invalid output type from an atom outside the order domain.
arg/3 distinguishes an uninstantiated compound term and a negative index from its ordinary
zero-or-out-of-range failure cases.
=../2 distinguishes partial lists from malformed list terms, requires an atomic one-element
construction list, and shares the advertised arity-255 limit with functor/3.
current_op/3 validates bound priority, specifier, and name filters before enumeration.
Its solutions come from the operator-table snapshot current when the goal starts, even if op/3
changes the live table between solutions.
clause/2 retains the Part 1 private-procedure rule for ordinary source, while ISO module bodies
expose their static and dynamic clauses through the Part 2 calling context. Attempts to retract a
static procedure raise a catchable modification permission error.
report(Rows) :-
forall(member(Name-Qty, Rows), format("~w~t~12|~t~d~4+~n", [Name, Qty])).
total(Rows, Total) :- aggregate_all(sum(Q), member(_-Q, Rows), Total).
initials(Name, Initial) :- sub_atom(Name, 0, 1, _, Initial).Terms are written in operator notation, and what writeq/1 produces reads back as the same term —
brackets and spacing are decided by priority and by whether two adjacent tokens would otherwise lex
as one. write_canonical/1 and write_term/2 with ignore_ops(true) opt out.
:- op(700, xfx, likes).
fact(alice likes bob). % read with the operator declared just above
?- fact(F), write(F). % alice likes bob
?- write_canonical(1+2*3). % +(1,*(2,3))An :- op/3 directive takes effect while the file is still being read, so the file that declares an
operator can use it. The reader and the writer share one table per engine, so nothing leaks between
two engines in the same process.
Predicates written in Prolog rather than C# live in a standard library that every engine loads at
construction, which costs about 220 µs. A consulted file that defines one of them replaces it
outright, so a program is free to write its own member/2 without inheriting extra solutions.
Strings are a distinct term type: string(S) is true of one, atom/1 is false, and the standard
order places them between numbers and atoms as SWI-Prolog 10 does. A string is interned beside the
atom text it shares, so unification is integer identity and a string survives every detached copy.
Double-quoted text reads as a list of characters by default (see Language modes),
and the string library reads such lists as text wherever SWI-Prolog's does, so
split_string("a,b", ",", "", Parts) works on it directly. Reading "..." as strings instead is
set_prolog_flag(double_quotes, string) — or the DotPrologFlags project property — away, outside
strict ISO mode.
bagof/3 and setof/3 group their solutions by whichever of the goal's variables are free —
those the caller can still see — and offer one group per binding of them. A variable is made
existential with ^/2, and one occurring only under \+ is never free, since negation proves a
goal but cannot bind anything. Both fail when the goal has no solutions, where findall/3 returns
[].
class(peter, a). class(ann, b). class(pat, a).
?- bagof(N, class(N, C), L). % C = a, L = [peter,pat] ; C = b, L = [ann]
?- bagof(N, C^class(N, C), L). % L = [peter,ann,pat]A definite clause grammar is translated into ordinary clauses when it is loaded — each non-terminal
gains the list before it and the list after it — so nothing about the engine knows that grammars
exist. {Goal} escapes to a plain goal, ! is a cut, \+ consumes nothing, and a pushback list
rewrites the input that the rules after it will see.
digits([D|T]) --> digit(D), digits(T).
digits([D]) --> digit(D).
digit(D) --> [D], { char_type(D, digit(_)) }.
number(N) --> digits(Ds), { number_chars(N, Ds) }.
?- phrase(number(N), "427", Rest). % N = 427, Rest = []phrase/2 and phrase/3 walk the control constructs themselves, so a body assembled at run time
works as well as one written as a rule. A rule asserted with assertz/1 is translated too.
Text streams carry terms and characters, while binary streams carry bytes. read/1 pulls text a
line at a time until the lexer finds a clause terminator, so a term can be read from a console as
soon as it is complete rather than after the input ends — a full stop inside 'a. b', inside
3.14, or inside =.. is not one. An incomplete clause at end of input is a catchable
syntax_error(unexpected_end_of_file) rather than a silently truncated term. When the
char_conversion flag is on, mappings installed by char_conversion/2 are applied to unquoted
input before tokenization; quoted text, escapes, and primitive character input remain raw. An input
stream's eof_action(error|eof_code|reset) controls reads after its first EOF marker, and
close/2 supports forced best-effort cleanup. open/3,4 rejects bound output arguments and alias
collisions before it touches the requested source/sink, and rejects non-source/sink terms with
domain_error(source_sink, Culprit). Host-invalid pathname atoms remain inside that same catchable
domain boundary. Stream permission errors preserve that alias or handle as the culprit, and
malformed handles cannot wrap to another live stream. Bound character-input targets are validated
before any character is consumed. Character, code, and byte predicates apply ISO error priority
when their stream and value arguments are both invalid. Open, close, stream positioning, and term
I/O likewise validate option shape, domains, and option semantics before stream lookup in the
standard order. Host reader and writer failures remain recoverable through catch/3 as
system_error; closing an already-disposed host stream follows the same policy, with force(true)
remaining best-effort. write_term/2,3 supports the ISO quoted, ignore_ops, numbervars, and
variable_names options. Boolean options are validated exactly, and variable_names/1 uses the
leftmost applicable name without binding its terms. read_term/2,3 rejects malformed options
before consuming the next term, and integer literals of any length read to their exact unbounded
value. It also enforces the advertised max_arity of
255 while reading, raising representation_error(max_arity) before constructing a larger compound.
Oversized float literals raise syntax_error(float_overflow) instead of becoming IEEE infinity;
underflow rounds to signed zero.
main :-
read_term(T, []),
( T == end_of_file -> true
; format("got ~q~n", [T]), main ).
?- with_output_to(atom(A), write(1+2)). % A = '1+2'
?- open('data.pl', read, S), read(S, T), close(S).Machine.Output remains what an embedding host sets, and it is user_output specifically — a
program that redirects itself with set_output/1 or with_output_to/2 cannot detach the host from
the stream it handed in. halt/0 flushes and closes whatever the program opened.
A standard module has an interface followed by zero or more, possibly non-contiguous bodies. The interface owns exports, re-exports, metapredicate declarations, and the initial operator, character-conversion, and flag state for every body:
:- module(shapes).
:- export(describe/1).
:- end_module(shapes).
:- body(shapes).
describe(N) :- helper(N). % shapes' own helper/1
helper(N) :- write(area(N)).
:- end_body(shapes).:- module(drawing).
:- export(draw/1).
:- end_module(drawing).
:- body(drawing).
:- import(shapes, describe/1).
draw(N) :- describe(N).
:- end_body(drawing).
?- drawing:draw(4). % area(4)
?- shapes:helper(9). % area(9), explicit qualificationAn exported predicate is also given its plain name when nothing else has claimed it, which is what
lets a generated facade, dotnet prolog run, and an embedding host call it without knowing modules
exist. A goal that is only known at run time carries its module and is resolved when called, so a
closure handed to a metapredicate finds the predicate it meant. :- metapredicate(run(:)) both
declares and exports a Part 2 metapredicate. current_module/1, current_predicate/1, and
predicate_property/2 inspect the calling module's visible database. Modern mode
also retains the compatibility declarations module/2, use_module/1,2, and meta_predicate/1;
StrictIso rejects those spellings in favor of the standard interface/body representation.
Nothing in the engine knows modules exist: resolution is a rewrite performed while loading.
A control term assembled at run time and passed to call/1 is lowered to VM bytecode through the
same control-construct compiler used for source clauses. Its cut is transparent within that
meta-called goal and opaque to the caller, as ISO specifies.
A non-callable term in a compiled clause body remains a runtime error: it is catchable if execution
reaches it, while normal control flow can leave it unevaluated.
call/2..8 checks the arity of the resulting goal; arity 255 is supported and 256 raises the
catchable representation_error(max_arity).
DotProlog's StrictIso mode implements ISO/IEC 13211-1:1995 with Technical Corrigenda 1–3,
ISO/IEC 13211-2:2000 modules, and ISO/IEC TS 13211-3:2025 definite clause grammars. The repository's
608-case Part 1 corpus and all 763 applicable declarations in the independent pinned Logtalk corpus
pass across consulted bytecode, generated C#, both cross-path directions, and NativeAOT. Parts 2
and 3 have licensed-text traceability and focused managed, generated, and NativeAOT coverage. This
is not a claim of SWI-Prolog compatibility. See
COMPATIBILITY.md and the
Part 1 and
Parts 2 and 3 traceability ledgers.
Diagnostic identifiers are stable and product-specific: DPL0xxx from the reader, DPL1xxx from
the compiler, DPL2xxx from .dpli contracts and code generation, and DPL3xxx from the linter.
hello.pl(4,12): error DPL0005: Expected '.' to end the clause but found 'b'.
$ dotnet new install DotProlog.Templates
$ dotnet new prolog-console -n HelloProlog
$ dotnet run --project HelloProlog
Hello from Prolog on .NET!prolog-console builds a Prolog program as a .NET application; prolog-lib builds a rule set as a
typed library for C#, F#, and VB; and prolog-test creates a Prolog test executable that runs under
dotnet test. A project pins the DotProlog SDK on the element itself:
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="DotProlog.Sdk" Version="0.15.0" />A .dplproj publishes with NativeAOT like any other project:
$ dotnet publish HelloProlog -c Release -r osx-arm64 -p:PublishAot=trueThe packages are published on NuGet.org as DotProlog.*, starting at 0.2.0 and currently at 0.15.0.
The commands above are also verified against a local feed built by dotnet pack, so they work
before a version ships.
Create a test project with dotnet new prolog-test. Any zero-arity predicate named test_* is a
test — it passes if it can be proved:
test_tier_boundaries :-
tier(1000, gold),
tier(999, silver).$ dotnet new prolog-test -n PricingTests
$ cd PricingTests
$ dotnet test --project PricingTests.dplproj
Test run summary: Passed!
total: 2
failed: 0
succeeded: 2Each test runs in a fresh engine, so one cannot see clauses another asserted.
The .NET 10 SDK selects Microsoft.Testing.Platform through global.json. The standalone
prolog-test template includes that setting. A solution containing Prolog tests should keep the
same test.runner setting in its solution-root global.json; this repository does so for its mixed
xUnit and Prolog test suite.
$ git clone https://github.com/kidoz/dotprolog.git
$ cd dotprolog
$ just check # format-check, docs, build, and test.NET SDK 10.0 or later is all that building and testing need; everything else restores from NuGet.
The documentation step in just check also needs uv.
Packages are DotProlog.*: DotProlog.Runtime, DotProlog.Syntax, DotProlog.Compiler,
DotProlog.Testing, DotProlog.Tool, DotProlog.Sdk, and DotProlog.Templates. DotProlog.Tool
is a pointer package: the executable ships in the per-RID DotProlog.Tool.linux-x64,
DotProlog.Tool.osx-arm64, and DotProlog.Tool.win-x64 packages, with DotProlog.Tool.any as the
portable fallback, so eleven IDs reach the feed in all. Every assembly-bearing package carries
Source Link and a symbol package, so a debugger can step from a package into the exact commit it
was built from.
just pack # every package into ./artifacts, with SHA256SUMSProlog.NET, which this project's brief originally proposed, is taken on NuGet by an unrelated
WAM-based .NET Prolog. DotProlog.* is what shipped instead.
Releasing is a tag. .github/workflows/release.yml runs on v*, re-runs the format, build, and
test gates against the tagged commit on all three platforms, checks the tag agrees with
VersionPrefix, publishes native binaries for Linux, Windows, and macOS, packs with checksums and
an SBOM, opens a GitHub release whose notes are the changelog section for that version, and only
then pushes to NuGet — from a separate job in a release environment, so a required reviewer can
stand between the tag and the feed.
The pinned independent ISO corpus is a CI-only step, run on one platform per push to main. Let CI
go green on the commit you intend to tag rather than relying on the release run to cover it.
Two things have to agree before tagging, and the workflow fails rather than guessing if they do
not: the tag must match VersionPrefix, and the changelog must contain a heading for that exact
version, because the release notes are extracted from it.
# 1. Move the version's changelog heading from "unreleased" to today's date, and add its link ref.
# 2. Set VersionPrefix in Directory.Build.props if the version is changing.
# 3. Commit and push, wait for CI, then:
git tag v0.15.0 && git push origin v0.15.0The publication job authenticates by trusted publishing: it
exchanges the workflow's OIDC token for a short-lived nuget.org key, so no API key is stored, and
the NUGET_USER secret names the account whose policy authorises the push.
A version whose GitHub release succeeded but whose packages never reached the feed can be finished without re-tagging — run the workflow manually and give it the version, and it publishes the packages already attached to that release. Re-running the original tag run would not help, because a re-run replays the workflow file as it was at the tag.
MIT — see LICENSE.
Author: Aleksandr Pavlov <ckidoz@gmail.com>
