Specification Generator
Systematically generate formal specifications from code and requirements. Produces preconditions, postconditions, invariants, and behavioral contracts that enable verification, documentation, and design-by-contract programming.
Core Capabilities
1. Precondition Generation
Define required conditions before execution:
- Parameter validation - Valid ranges, types, states
- Object state requirements - Required object states
- Resource availability - Files exist, connections open
- Authorization checks - Permission requirements
- Null safety - Non-null parameter requirements
- Invariant satisfaction - Class invariants must hold
2. Postcondition Generation
Specify guaranteed outcomes after execution:
- Return value constraints - Valid return ranges and properties
- State modifications - How object state changes
- Side effects - File writes, database updates
- Exception conditions - When exceptions are thrown
- Resource management - Resources properly released
- Invariant preservation - Invariants still hold
3. Invariant Generation
Identify properties that always hold:
- Class invariants - Properties true for all instances
- Loop invariants - Properties preserved by iterations
- Data structure invariants - Consistency conditions
- Representation invariants - Internal consistency
- Temporal invariants - Time-based properties
4. Contract Generation
Create complete behavioral specifications:
- Method contracts - Full pre/post conditions
- Interface contracts - API behavioral guarantees
- Type contracts - Type-level properties
- Protocol contracts - Valid call sequences
- Frame conditions - What doesn't change
Specification Generation Workflow
Step 1: Analyze Code or Requirements
Understand what needs to be specified:
For existing code:
- Read function/method implementation
- Identify parameters and return type
- Find state modifications
- Detect resource usage
- Locate error conditions
For requirements:
- Extract functional requirements
- Identify constraints and rules
- Find boundary conditions
- Determine error scenarios
- Specify expected behavior
Example code:
def withdraw(account, amount):
if account.balance < amount:
raise InsufficientFundsError()
account.balance -= amount
return account.balance
Analysis:
- Parameters: account (object), amount (number)
- Return: new balance (number)
- State change: account.balance decreases
- Error: raises exception if insufficient funds
- Constraints: amount should be positive, account must exist
Step 2: Identify Contract Elements
Determine what to specify:
Preconditions (requires/assume):
- What must be true before execution?
- Parameter validity requirements
- Object state requirements
Postconditions (ensures/guarantee):
- What is guaranteed after execution?
- Return value properties
- State changes
Invariants:
- What is always true?
- Class-level properties
- Loop properties
Example identification:
# withdraw function
Preconditions:
- account is not None
- amount > 0
- account.balance >= amount (to succeed without exception)
Postconditions:
- If successful: account.balance == old(account.balance) - amount
- If successful: return value == new balance
- If insufficient: raises InsufficientFundsError
- account object still valid
Invariants (Account class):
- balance >= 0 (always)
- account_number is immutable
Step 3: Choose Specification Language
Select appropriate format:
Specification languages:
| Language | Use Case | Example | |----------|----------|---------| | JML (Java) | Java programs | //@ requires, //@ ensures | | ACSL (C) | C programs | /@ requires, /@ ensures | | Dafny | Verified code | requires, ensures | | Eiffel | Design-by-contract | require, ensure | | Python docstring | Documentation | :param:, :raises:, :returns: | | TypeScript | Type annotations | Type predicates, assertions | | Hoare logic | Formal proofs | {P} S {Q} notation |
Example language choice:
- Java method → Use JML
- C function → Use ACSL
- Python function → Use docstring assertions
- Verification task → Use Dafny
Step 4: Write Specifications
Generate formal contracts:
Format depends on language:
JML (Java):
/*@ requires amount > 0;
@ requires account != null;
@ requires account.balance >= amount;
@ ensures account.balance == \old(account.balance) - amount;
@ ensures \result == account.balance;
@ signals (InsufficientFundsError e) account.balance < amount;
@*/
public int withdraw(Account account, int amount) {
// implementation
}
ACSL (C):
/*@ requires amount > 0;
@ requires \valid(account);
@ requires account->balance >= amount;
@ ensures account->balance == \old(account->balance) - amount;
@ ensures \result == account->balance;
@*/
int withdraw(Account *account, int amount) {
// implementation
}
Dafny:
method Withdraw(account: Account, amount: int) returns (newBalance: int)
requires amount > 0
requires account.balance >= amount
ensures account.balance == old(account.balance) - amount
ensures newBalance == account.balance
modifies account
{
// implementation
}
Python (docstring):
def withdraw(account: Account, amount: float) -> float:
"""
Withdraw amount from account.
:param account: The account to withdraw from
:param amount: The amount to withdraw (must be positive)
:raises InsufficientFundsError: If balance < amount
:raises ValueError: If amount <= 0
:returns: The new account balance
Preconditions:
- account is not None
- amount > 0
- account.balance >= amount (for successful withdrawal)
Postconditions:
- account.balance == old_balance - amount
- return value == account.balance
- If insufficient funds: InsufficientFundsError raised
- account.balance >= 0 (invariant preserved)
"""
# implementation
Step 5: Verify Specifications
Ensure specifications are correct:
Verification steps:
- Check preconditions are sufficient (not too strong)
- Verify postconditions are achievable (not too weak)
- Confirm invariants always hold
- Test specifications with examples
- Use formal verification tools if available
Example verification:
# Test preconditions
assert amount > 0 # Valid
assert account is not None # Valid
assert account.balance >= amount # Valid for success case
# Test postconditions
old_balance = account.balance
result = withdraw(account, 100)
assert account.balance == old_balance - 100 # Check
assert result == account.balance # Check
# Test invariants
assert account.balance >= 0 # Should always hold
Specification Patterns
Pattern 1: Simple Function Contract
Code:
def sqrt(x):
"""Compute square root."""
return x ** 0.5
Generated specification:
def sqrt(x: float) -> float:
"""
Compute the square root of x.
:param x: The number to compute square root of
:returns: The square root of x
:raises ValueError: If x < 0
Preconditions:
- x >= 0
Postconditions:
- result >= 0
- abs(result * result - x) < 1e-10 # Precision tolerance
- If x == 0, then result == 0
- If x == 1, then result == 1
"""
if x < 0:
raise ValueError("Cannot compute square root of negative number")
return x ** 0.5
JML version:
/*@ requires x >= 0;
@ ensures \result >= 0;
@ ensures Math.abs(\result * \result - x) < 1e-10;
@ ensures x == 0 ==> \result == 0;
@ ensures x == 1 ==> \result == 1;
@ signals (IllegalArgumentException e) x < 0;
@*/
public double sqrt(double x) {
if (x < 0) throw new IllegalArgumentException();
return Math.sqrt(x);
}