Grammar#
The PyAB experiment language is a domain-specific language for defining A/B tests. It follows
a C-style syntax while maintaining Python-like simplicity.
Referenced by the Language pyab_experiment.language module
Basic Structure#
Each experiment is defined using this basic structure:
def experiment_name {
[salt: "optional_salt"]
[splitters: field1, field2, ...]
conditional_logic
}
Language Components#
Return Statements#
Return statements define groups with weighted probabilities:
return "control" weighted 1,
"variant_a" weighted 2,
"variant_b" weighted 1
This means: - variant_a has 2x the probability of being selected compared to control or variant_b - Total weights: 4 (1+2+1), so probabilities are:
control: 25%
variant_a: 50%
variant_b: 25%
Conditional Logic#
The language supports nested if/else if/else statements:
if user_id in (1,2,3) {
return "group1" weighted 1
} else if country == "US" and age >= 18 {
return "group2" weighted 1
} else {
return "default" weighted 1
}
Supported Operators#
Comparison:
==,!=,>,<,>=,<=,in,not inLogical:
and,or,not- Values can be:
Strings:
"value"Numbers:
42,3.14,-1Tuples:
(1,2,3)
Complete Example#
Here’s a real-world example with annotations:
def complex_experiment {
// Salt ensures consistent group assignment
salt: "user_exp_v1"
// Fields used for splitting traffic
splitters: user_id, country
// Target specific user segments
if age >= 21 and country in ("US", "CA") {
// High-value markets get 3 variants
return "control" weighted 1,
"variant_a" weighted 2,
"variant_b" weighted 2
} else if country not in ("US", "CA") {
// International markets get 2 variants
return "int_control" weighted 1,
"int_variant" weighted 1
} else {
// Everyone else gets default experience
return "default" weighted 1
}
}
Formal Grammar#
The full grammar rules as defined in the language:
<S> ::= <header>
<header> ::= <header_id> "{" <opt_header_salt> <opt_splitter> <conditional> "}"
<empty> ::=
<header_id> ::= KW_DEF <ID>
<opt_header_salt> ::= KW_SALT ":" <STRING_LITERAL>
| <empty>
<opt_splitter> ::= KW_SPLITTERS ":" <fields>
| <empty>
<fields> ::= <ID>
| <ID> "," <fields>
<conditional> ::= <return_expr>
| KW_IF <predicate> "{" <conditional> "}" <subconditional>
<subconditional> ::=
| KW_ELSE "{" <conditional> "}"
| KW_ELIF <predicate> "{" <conditional> "}" <subconditional>
<predicate> ::= KW_NOT <predicate>
| <predicate> KW_OR <predicate>
| <predicate> KW_AND <predicate>
| "(" <predicate> ")"
| <term> <logical_op> <term>
Terminal Tokens#
Special Symbols#
LPAREN : \(
RPAREN : \)
MINUS : -
COMMA : ,
COLON : :
LBRACE : {
RBRACE : }
Logical Operators#
KW_EQ : ==
KW_GT : >
KW_LT : <
KW_GE : >=
KW_LE : <=
KW_NE : !=
KW_IN : in
KW_NOT_IN : not\s+in
KW_NOT : not
Reserved Keywords#
KW_DEF : def
KW_SALT : salt
KW_SPLITTERS : splitters
KW_IF : if
KW_ELIF : else\s*if
KW_ELSE : else
KW_WEIGHTED : weighted
KW_RETURN : return
KW_AND : and
KW_OR : or
Notes#
All numeric literals must be non-negative (minus sign handled separately)
String literals support both single and double quotes
Keywords are case-sensitive
Block comments support nesting through state management
Whitespace is significant for some operators (e.g., ‘not in’)
Comments#
Supports both C-style comments: