Design: a Python-like surface syntax (.ppy)¶
Status: agreed design (#105). Implementation:
#106; the parser and the pretty printer exist
(prodigy/pgcl/ppy_parser.py, prodigy/pgcl/ppy_printer.py), the CLI and the GUI still use
.pgcl. Semantics of nested inference: #104.
Goals¶
Programs that read like Python, but keep curly-brace blocks and semicolons.
Both syntaxes produce the same AST. The type checker, the compiler and the analysis do not change, except for the new constructs listed below.
.pgclkeeps working. New programs use.ppy. A converter (prodigy convert) translates.pgclinto.ppy. In the long run.ppybecomes the default; the old syntax is retired on purpose, with an announcement.The syntax can express everything. The analysis may still answer “not implemented” for cases it cannot compute exactly yet. Such cases end with a clear error, never with a wrong result.
Overview¶
Construct |
|
|
|---|---|---|
Blocks |
|
|
Assignment |
|
|
Equality |
|
|
Boolean operators |
|
|
Power |
|
|
Division |
|
|
Declaration |
|
|
Several variables |
|
|
Initial value |
assignment after the declarations |
|
Bounds |
|
|
Parameters |
|
|
Constants |
|
|
|
|
|
Binary choice |
|
|
Choice with several branches |
nested |
|
Categorical distribution |
|
|
Functions |
|
|
Calls |
|
|
Nested inference |
|
|
Queries |
|
|
skip, abort, while, loop(n), observe(g), tick(e), the built-in distributions (unif,
geometric, poisson, binomial, bernoulli, iid, …) and Iverson brackets [g] stay as they
are. Comments start with #. // is floor division in .ppy, so it no longer starts a comment.
Decisions in detail¶
Blocks and statements¶
Curly braces delimit blocks, and semicolons end statements, as today. Indentation carries no meaning. This keeps error messages, pasting into the editor and one-line expressions (invariants, answers of language models) simple.
Operators¶
= assigns, == compares. Boolean operators are accepted in both spellings: and/or/not and
&&/||/!. The canonical spelling, used by the converter and the pretty printer, is
and/or/not. ! binds like not, as in Python weaker than the comparisons: !x == y means
!(x == y). Comparisons do not chain; a < b < c is a syntax error.
/ is rational division, as in Python. In guards, x/2 <= 1 means x <= 2. An assignment to a
nat variable requires a natural result and is rejected otherwise; it never rounds (see the
section “Division” in the grammar page). // is floor division, as in Python (7 // 2 == 3). It
rounds the same way in guards and in assignments. As an update of a generating function it needs
its own exact implementation, via the residue classes modulo the divisor, as for x % q.
Declarations and scope¶
x, y: nat;
z: nat[0, 10] = 3;
const N: nat = 10;
param p: real;
Declarations may appear anywhere. A variable is visible from its declaration to the end of the enclosing block.
A declaration without a value gives 0. A declaration inside a loop body is executed in every iteration, so the variable starts again from its initial value.
At the end of its block, a variable is marginalised out of the generating function. This keeps generating functions small, and helper variables of a loop body do not appear in the loop’s invariant. Variables needed at the end of the program are declared at the outermost level.
Every variable must be declared. Nothing is assumed implicitly. Using a variable before its declaration is an error.
At the top level, declarations before the first statement declare the program’s inputs, as in
.pgcl: without a value they start at 0 unless an input distribution is given. A declaration after the first statement always starts at 0 (or at its value).const,paramanddefare allowed at the top level only.
Constants and parameters¶
const N: nat = 10;is a known value. The compiler substitutes it everywhere: in expressions,loop(N), bounds such asnat[0, N]and arguments such asunif(1, N). It never appears in the generating function.param p: real;is an unknown symbol. Prodigy computes with it symbolically, e.g. for template and parameter synthesis.
Conditionals¶
if (x > 3) {
y = 1;
} else if (x > 1) {
y = 2;
}
else is optional; a missing else means else { skip }. else if chains are allowed.
Probabilistic choice¶
A choice with several branches is written as sampling a variable and switching on it:
c: nat = categorical({1: 1/6, 2: 1/3, 3: 1/2});
switch (c) {
case 1: { x = 0; }
case 2, 3: { x = 1; }
default: { x = 2; }
}
Every
caseis a block of its own. There is no fall-through. Several values are listed with commas.defaultis optional.Semantically, each
casefilters onc == k, and the results of the branches are added. This is computed exactly with the existing operations.switchworks on every variable, not only on sampled ones. It takes a variable, not an expression, because eachcaserefers to it again.If
cis declared in a block and not used after theswitch, it is marginalised out at the end of the block (see “Declarations and scope”), so the extra variable costs nothing afterwards.{A} [p] {B}stays as shorthand for the binary case.
Categorical distribution¶
Main form:
categorical({v1: p1, v2: p2, …}), pairs of value and probability as in a Python dict.Short form:
categorical(p0, p1, …, pn)for the values0, 1, …, n.The probabilities must sum to 1. For literal probabilities the parser checks this. For now the probabilities must be literals; probabilities with parameters, which the analysis would have to check, are not implemented yet.
Functions¶
def f(a: nat, b: nat) -> nat {
s: nat = a + b;
return s;
}
x = f(2, 3);
y = f(2, b = 3);
Arguments are passed by position, optionally by name. Named arguments come after the positional ones.
Nested inference¶
d: dist = infer(f, 10);
x = sample(d);
infer(f, args…)takes a function and its arguments and gives the normalised output distribution of the call as a value of typedist.sample(d)draws independently from it.The syntax allows every case. For cases the analysis cannot handle yet, it ends with “not implemented” and a clear message. The semantics is designed in #104. Example: an argument that depends on the random state together with
observeinf, where the normalising constant depends on that argument.
Queries¶
query Pr[x == 3];
query Ex[x + y];
print;
plot(x, y);
Queries are statements at the end of the program, introduced by keywords instead of the prefixes
? and !. No other statement may follow a query.
Example side by side¶
.pgcl:
nat n;
nat m;
nat c;
rparam p;
while (0 < n) {
{ m := m + 1 } [p] { m := m + 2 }
c := 1 : 1/2 + 2 : 1/4 + 3 : 1/4;
if (c = 1) { n := n - 1 } else { if (c = 2) { m := 0 } else { skip } }
}
?Pr[m = 5]
.ppy:
n, m: nat;
param p: real;
while (n > 0) {
{m = m + 1;} [p] {m = m + 2;}
c: nat = categorical({1: 1/2, 2: 1/4, 3: 1/4});
switch (c) {
case 1: { n = n - 1; }
case 2: { m = 0; }
}
}
query Pr[m == 5];
In the .ppy version, c is declared inside the loop body, so it is marginalised out at the end of
every iteration and does not appear in the loop invariant. In the .pgcl version it is a variable
of the whole program.
Still open¶
How
distvalues appear in the result JSON and in the GUI (#104).The exact form of
plotandoptimize.Whether the
.pgclfiles inpgfexamples/are converted in the repository, and when the old syntax is retired.
Notes for the converter¶
Comments are not part of the AST; the converter has to carry them over separately or loses them.
Variables whose names are keywords in
.ppy(switch,case,default,def,param,print, …) must be renamed, e.g.switchinpgfexamples/inference/loop_free/conditioning/monty_hall_nested.pgcl. The pretty printer refuses such names.