/**
* Tests for PDSL Parser
*/
import { describe, it, expect } from '@jest/globals';
import { tokenize } from './probabilisticLexer.js';
import { parse } from './probabilisticParser.js';
import { validate } from './probabilisticValidator.js';
import { generate } from './problogGenerator.js';
import { Program } from './probabilisticAST.js';
// ============================================================================
// Helper Functions
// ============================================================================
function parseSource(source: string): Program {
const tokens = tokenize(source);
return parse(tokens);
}
function fullPipeline(source: string): {
ast: Program;
valid: boolean;
problog: string;
} {
const ast = parseSource(source);
const validation = validate(ast);
const problog = generate(ast);
return {
ast,
valid: validation.valid,
problog,
};
}
// ============================================================================
// Lexer Tests
// ============================================================================
describe('Probabilistic Lexer', () => {
it('should tokenize probabilities', () => {
const tokens = tokenize('0.7 0.5 1.0 0');
expect(tokens[0].type).toBe('PROBABILITY');
expect(tokens[0].value).toBe('0.7');
expect(tokens[1].value).toBe('0.5');
});
it('should tokenize keywords', () => {
const tokens = tokenize('probabilistic_model observe query learn');
expect(tokens[0].type).toBe('PROBABILISTIC_MODEL');
expect(tokens[1].type).toBe('OBSERVE');
expect(tokens[2].type).toBe('QUERY');
expect(tokens[3].type).toBe('LEARN');
});
it('should distinguish variables from constants', () => {
const tokens = tokenize('X alice Bird sparrow');
expect(tokens[0].type).toBe('VARIABLE');
expect(tokens[0].value).toBe('X');
expect(tokens[1].type).toBe('CONSTANT');
expect(tokens[1].value).toBe('alice');
expect(tokens[2].type).toBe('VARIABLE');
expect(tokens[3].type).toBe('CONSTANT');
});
it('should tokenize operators', () => {
const tokens = tokenize(':: :- , ;');
expect(tokens[0].type).toBe('PROB_ANNOTATION');
expect(tokens[1].type).toBe('IMPLICATION');
expect(tokens[2].type).toBe('COMMA');
expect(tokens[3].type).toBe('SEMICOLON');
});
it('should handle comments', () => {
const tokens = tokenize('0.5 :: rain # This is a comment\nquery rain');
const types = tokens.map(t => t.type);
expect(types).not.toContain('COMMENT');
});
it('should tokenize strings', () => {
const tokens = tokenize('"medical_data.csv"');
expect(tokens[0].type).toBe('STRING');
expect(tokens[0].value).toBe('medical_data.csv');
});
});
// ============================================================================
// Parser Tests - Basic Constructs
// ============================================================================
describe('Probabilistic Parser - Basic Constructs', () => {
it('should parse probabilistic facts', () => {
const source = `
probabilistic_model Test {
0.7 :: sunny
}
`;
const ast = parseSource(source);
expect(ast.models).toHaveLength(1);
expect(ast.models[0].statements).toHaveLength(1);
expect(ast.models[0].statements[0].type).toBe('ProbabilisticFact');
});
it('should parse probabilistic rules', () => {
const source = `
probabilistic_model Test {
0.9 :: flies(X) :- bird(X)
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
expect(stmt.type).toBe('ProbabilisticRule');
if (stmt.type === 'ProbabilisticRule') {
expect(stmt.probability).toBe(0.9);
expect(stmt.head.predicate).toBe('flies');
expect(stmt.body).toHaveLength(1);
}
});
it('should parse deterministic facts', () => {
const source = `
probabilistic_model Test {
bird(sparrow)
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
expect(stmt.type).toBe('DeterministicFact');
});
it('should parse observations', () => {
const source = `
probabilistic_model Test {
observe fever
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
expect(stmt.type).toBe('Observation');
if (stmt.type === 'Observation') {
expect(stmt.literal.negated).toBe(false);
}
});
it('should parse negated observations', () => {
const source = `
probabilistic_model Test {
observe not raining
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
if (stmt.type === 'Observation') {
expect(stmt.literal.negated).toBe(true);
}
});
it('should parse queries', () => {
const source = `
probabilistic_model Test {
query disease
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
expect(stmt.type).toBe('Query');
});
it('should parse annotated disjunctions', () => {
const source = `
probabilistic_model Test {
0.3 :: a; 0.7 :: b
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
expect(stmt.type).toBe('AnnotatedDisjunction');
if (stmt.type === 'AnnotatedDisjunction') {
expect(stmt.choices).toHaveLength(2);
expect(stmt.choices[0].probability).toBe(0.3);
expect(stmt.choices[1].probability).toBe(0.7);
}
});
it('should parse learning directives', () => {
const source = `
probabilistic_model Test {
learn parameters from dataset("data.csv")
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
expect(stmt.type).toBe('LearningDirective');
if (stmt.type === 'LearningDirective') {
expect(stmt.dataset).toBe('data.csv');
}
});
});
// ============================================================================
// Parser Tests - Complex Constructs
// ============================================================================
describe('Probabilistic Parser - Complex Constructs', () => {
it('should parse rules with multiple body literals', () => {
const source = `
probabilistic_model Test {
0.9 :: flies(X) :- bird(X), not penguin(X)
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
if (stmt.type === 'ProbabilisticRule') {
expect(stmt.body).toHaveLength(2);
expect(stmt.body[0].negated).toBe(false);
expect(stmt.body[1].negated).toBe(true);
}
});
it('should parse atoms with multiple arguments', () => {
const source = `
probabilistic_model Test {
parent(alice, bob)
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
if (stmt.type === 'DeterministicFact') {
expect(stmt.atom.args).toHaveLength(2);
expect(stmt.atom.args[0].type).toBe('Constant');
expect(stmt.atom.args[1].type).toBe('Constant');
}
});
it('should parse nested atoms', () => {
const source = `
probabilistic_model Test {
grandparent(X, Z) :- parent(X, Y), parent(Y, Z)
}
`;
const ast = parseSource(source);
const stmt = ast.models[0].statements[0];
if (stmt.type === 'ProbabilisticRule') {
expect(stmt.body).toHaveLength(2);
}
});
it('should parse multiple models', () => {
const source = `
probabilistic_model Model1 {
0.5 :: a
}
probabilistic_model Model2 {
0.6 :: b
}
`;
const ast = parseSource(source);
expect(ast.models).toHaveLength(2);
expect(ast.models[0].name).toBe('Model1');
expect(ast.models[1].name).toBe('Model2');
});
});
// ============================================================================
// Validator Tests
// ============================================================================
describe('Probabilistic Validator', () => {
it('should detect invalid probabilities', () => {
const source = `
probabilistic_model Test {
1.5 :: invalid
}
`;
const ast = parseSource(source);
const result = validate(ast);
expect(result.valid).toBe(false);
expect(result.errors[0].type).toBe('InvalidProbability');
});
it('should detect annotated disjunction sum > 1', () => {
const source = `
probabilistic_model Test {
0.6 :: a; 0.5 :: b
}
`;
const ast = parseSource(source);
const result = validate(ast);
expect(result.valid).toBe(false);
expect(result.errors[0].type).toBe('InvalidAnnotatedDisjunction');
});
it('should accept annotated disjunction sum = 1', () => {
const source = `
probabilistic_model Test {
0.6 :: a; 0.4 :: b
}
`;
const ast = parseSource(source);
const result = validate(ast);
expect(result.valid).toBe(true);
});
it('should detect unsafe variables in rules', () => {
const source = `
probabilistic_model Test {
0.9 :: flies(X) :- bird(Y)
}
`;
const ast = parseSource(source);
const result = validate(ast);
expect(result.valid).toBe(false);
expect(result.errors[0].type).toBe('UnsafeVariable');
});
it('should accept safe rules', () => {
const source = `
probabilistic_model Test {
0.9 :: flies(X) :- bird(X)
}
`;
const ast = parseSource(source);
const result = validate(ast);
expect(result.valid).toBe(true);
});
it('should detect arity mismatches', () => {
const source = `
probabilistic_model Test {
person(alice)
person(bob, 30)
}
`;
const ast = parseSource(source);
const result = validate(ast);
expect(result.valid).toBe(false);
expect(result.errors[0].type).toBe('ArityMismatch');
});
});
// ============================================================================
// ProbLog Generator Tests
// ============================================================================
describe('ProbLog Generator', () => {
it('should generate probabilistic facts', () => {
const source = 'probabilistic_model T { 0.7 :: rain }';
const { problog } = fullPipeline(source);
expect(problog).toContain('0.7::rain.');
});
it('should generate probabilistic rules', () => {
const source = 'probabilistic_model T { 0.9 :: flies(X) :- bird(X) }';
const { problog } = fullPipeline(source);
expect(problog).toContain('0.9::flies(X) :- bird(X).');
});
it('should translate negation to \\+', () => {
const source = 'probabilistic_model T { 0.9 :: flies(X) :- bird(X), not penguin(X) }';
const { problog } = fullPipeline(source);
expect(problog).toContain('\\+ penguin(X)');
});
it('should translate observations to evidence', () => {
const source = 'probabilistic_model T { observe fever }';
const { problog } = fullPipeline(source);
expect(problog).toContain('evidence(fever, true).');
});
it('should translate negated observations', () => {
const source = 'probabilistic_model T { observe not raining }';
const { problog } = fullPipeline(source);
expect(problog).toContain('evidence(raining, false).');
});
it('should translate queries', () => {
const source = 'probabilistic_model T { query flu }';
const { problog } = fullPipeline(source);
expect(problog).toContain('query(flu).');
});
it('should preserve annotated disjunctions', () => {
const source = 'probabilistic_model T { 0.3 :: a; 0.7 :: b }';
const { problog } = fullPipeline(source);
expect(problog).toContain('0.3::a; 0.7::b.');
});
});
// ============================================================================
// Example Tests from Documentation
// ============================================================================
describe('PDSL Examples from Documentation', () => {
it('should parse Example 1: Coin Flip', () => {
const source = `
probabilistic_model CoinFlip {
0.5 :: heads
0.5 :: tails
query heads
}
`;
const { valid, problog } = fullPipeline(source);
expect(valid).toBe(true);
expect(problog).toContain('0.5::heads.');
expect(problog).toContain('query(heads).');
});
it('should parse Example 3: Bird Flight', () => {
const source = `
probabilistic_model BirdFlight {
bird(sparrow)
bird(penguin)
penguin(penguin)
0.95 :: flies(X) :- bird(X), not penguin(X)
query flies(sparrow)
}
`;
const { valid, problog } = fullPipeline(source);
expect(valid).toBe(true);
expect(problog).toContain('bird(sparrow).');
expect(problog).toContain('\\+ penguin(X)');
});
it('should parse Example 4: Simple Diagnosis', () => {
const source = `
probabilistic_model SimpleDiagnosis {
0.01 :: flu
0.9 :: fever :- flu
0.05 :: fever :- not flu
observe fever
query flu
}
`;
const { valid, problog } = fullPipeline(source);
expect(valid).toBe(true);
expect(problog).toContain('evidence(fever, true).');
});
it('should parse Example 7: Simple Weather', () => {
const source = `
probabilistic_model SimpleWeather {
0.2 :: rain
0.9 :: cloudy :- rain
0.3 :: cloudy :- not rain
observe cloudy
query rain
}
`;
const { valid, problog } = fullPipeline(source);
expect(valid).toBe(true);
});
it('should parse Example 2: Biased Die (Annotated Disjunction)', () => {
const source = `
probabilistic_model BiasedDie {
0.1 :: roll(1);
0.1 :: roll(2);
0.1 :: roll(3);
0.15 :: roll(4);
0.15 :: roll(5);
0.4 :: roll(6)
query roll(6)
}
`;
const { valid, problog } = fullPipeline(source);
expect(valid).toBe(true);
expect(problog).toContain('0.1::roll(1); 0.1::roll(2)');
});
});
// ============================================================================
// Error Handling Tests
// ============================================================================
describe('Parser Error Handling', () => {
it('should throw error for invalid syntax', () => {
const source = 'probabilistic_model T { 0.5 }';
expect(() => parseSource(source)).toThrow();
});
it('should throw error for missing braces', () => {
const source = 'probabilistic_model T 0.5 :: rain';
expect(() => parseSource(source)).toThrow();
});
it('should throw error for invalid probability annotation', () => {
const source = 'probabilistic_model T { 0.5 : rain }';
expect(() => parseSource(source)).toThrow();
});
});
// ============================================================================
// Integration Tests
// ============================================================================
describe('Full Pipeline Integration', () => {
it('should handle complete medical diagnosis example', () => {
const source = `
probabilistic_model MedicalDiagnosis {
# Priors
0.01 :: flu
0.001 :: covid
# Symptoms
0.9 :: fever :- flu
0.95 :: fever :- covid
0.1 :: fever
# Evidence
observe fever
# Query
query flu
query covid
}
`;
const { valid, problog } = fullPipeline(source);
expect(valid).toBe(true);
expect(problog).toContain('0.01::flu.');
expect(problog).toContain('evidence(fever, true).');
expect(problog).toContain('query(flu).');
expect(problog).toContain('query(covid).');
});
it('should handle social network example', () => {
const source = `
probabilistic_model SocialNetwork {
person(alice)
person(bob)
0.3 :: friends(alice, bob)
friends(X, Y) :- friends(Y, X)
0.7 :: connected(X, Y) :- friends(X, Y)
query connected(alice, bob)
}
`;
const { valid, problog } = fullPipeline(source);
expect(valid).toBe(true);
});
});