use crate::formatter::types::*;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ContractSystem {
type_env: HashMap<String, ShellType>,
function_sigs: HashMap<String, FunctionSignature>,
active_contracts: Vec<Contract>,
inference_engine: TypeInferenceEngine,
}
#[derive(Debug, Clone)]
pub struct FunctionSignature {
pub name: String,
pub parameters: Vec<Parameter>,
pub return_type: ShellType,
pub preconditions: Vec<Contract>,
pub postconditions: Vec<Contract>,
}
#[derive(Debug, Clone)]
pub struct Parameter {
pub name: String,
pub param_type: ShellType,
pub is_optional: bool,
}
#[derive(Debug, Clone)]
pub struct Contract {
pub kind: ContractKind,
pub condition: ContractCondition,
pub description: String,
pub location: Span,
}
#[derive(Debug, Clone)]
pub enum ContractCondition {
TypeConstraint {
var: String,
expected_type: ShellType,
},
RangeConstraint {
var: String,
min: Option<i64>,
max: Option<i64>,
},
NonNull {
var: String,
},
FileSystemConstraint {
path: String,
constraint: FsConstraint,
},
CustomPredicate {
expression: String,
},
And(Box<ContractCondition>, Box<ContractCondition>),
Or(Box<ContractCondition>, Box<ContractCondition>),
Not(Box<ContractCondition>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FsConstraint {
Exists,
IsReadable,
IsWritable,
IsExecutable,
IsDirectory,
IsRegularFile,
}
#[derive(Debug, Clone, Default)]
pub struct TypeInferenceEngine {
constraints: Vec<TypeConstraint>,
next_type_var: u32,
}
#[derive(Debug, Clone)]
pub struct TypeConstraint {
pub left: ShellType,
pub right: ShellType,
pub location: Span,
pub reason: ConstraintReason,
}
#[derive(Debug, Clone)]
pub enum ConstraintReason {
Assignment,
FunctionCall,
Arithmetic,
Comparison,
ArrayAccess,
}
impl ContractSystem {
pub fn new() -> Self {
Self {
type_env: HashMap::new(),
function_sigs: Self::builtin_functions(),
active_contracts: Vec::new(),
inference_engine: TypeInferenceEngine::default(),
}
}
fn builtin_functions() -> HashMap<String, FunctionSignature> {
let mut functions = HashMap::new();
functions.insert(
"echo".to_string(),
FunctionSignature {
name: "echo".to_string(),
parameters: vec![Parameter {
name: "args".to_string(),
param_type: ShellType::Array(Box::new(ShellType::String)),
is_optional: true,
}],
return_type: ShellType::ExitCode,
preconditions: vec![],
postconditions: vec![],
},
);
functions.insert(
"test".to_string(),
FunctionSignature {
name: "test".to_string(),
parameters: vec![Parameter {
name: "expression".to_string(),
param_type: ShellType::String,
is_optional: false,
}],
return_type: ShellType::Boolean,
preconditions: vec![],
postconditions: vec![],
},
);
functions.insert(
"read".to_string(),
FunctionSignature {
name: "read".to_string(),
parameters: vec![Parameter {
name: "variables".to_string(),
param_type: ShellType::Array(Box::new(ShellType::String)),
is_optional: true,
}],
return_type: ShellType::ExitCode,
preconditions: vec![],
postconditions: vec![Contract {
kind: ContractKind::Postcondition,
condition: ContractCondition::CustomPredicate {
expression: "read variables are defined".to_string(),
},
description: "Variables are defined after successful read".to_string(),
location: Span::new(BytePos(0), BytePos(0)),
}],
},
);
functions
}
pub fn add_contract(&mut self, contract: Contract) {
self.active_contracts.push(contract);
}
pub fn infer_variable_type(&mut self, var_name: &str, context: &TypeContext) -> ShellType {
if let Some(existing_type) = self.type_env.get(var_name) {
return existing_type.clone();
}
let type_var = self.inference_engine.fresh_type_var();
match context {
TypeContext::Assignment { value_type } => {
self.inference_engine.add_constraint(TypeConstraint {
left: type_var.clone(),
right: value_type.clone(),
location: context.location(),
reason: ConstraintReason::Assignment,
});
}
TypeContext::FunctionCall {
function,
param_index,
} => {
if let Some(sig) = self.function_sigs.get(function) {
if let Some(param) = sig.parameters.get(*param_index) {
self.inference_engine.add_constraint(TypeConstraint {
left: type_var.clone(),
right: param.param_type.clone(),
location: context.location(),
reason: ConstraintReason::FunctionCall,
});
}
}
}
TypeContext::Arithmetic => {
self.inference_engine.add_constraint(TypeConstraint {
left: type_var.clone(),
right: ShellType::Integer,
location: context.location(),
reason: ConstraintReason::Arithmetic,
});
}
}
self.type_env.insert(var_name.to_string(), type_var.clone());
type_var
}
pub fn solve_constraints(&mut self) -> Result<(), TypeError> {
let mut substitution = HashMap::new();
for constraint in &self.inference_engine.constraints {
match self.unify(&constraint.left, &constraint.right, &mut substitution) {
Ok(()) => {}
Err(e) => {
return Err(TypeError {
kind: e,
location: constraint.location,
constraint_reason: constraint.reason.clone(),
})
}
}
}
let mut updated_env = HashMap::new();
for (var, var_type) in &self.type_env {
let new_type = self.apply_substitution(var_type, &substitution);
updated_env.insert(var.clone(), new_type);
}
self.type_env = updated_env;
Ok(())
}
#[allow(clippy::only_used_in_recursion)]
fn unify(
&self,
t1: &ShellType,
t2: &ShellType,
substitution: &mut HashMap<u32, ShellType>,
) -> Result<(), TypeErrorKind> {
match (t1, t2) {
(a, b) if a == b => Ok(()),
(ShellType::TypeVar(id), other) | (other, ShellType::TypeVar(id)) => {
if let Some(existing) = substitution.get(id).cloned() {
self.unify(&existing, other, substitution)
} else {
substitution.insert(*id, other.clone());
Ok(())
}
}
(ShellType::Array(inner1), ShellType::Array(inner2)) => {
self.unify(inner1, inner2, substitution)
}
(
ShellType::AssocArray { key: k1, value: v1 },
ShellType::AssocArray { key: k2, value: v2 },
) => {
self.unify(k1, k2, substitution)?;
self.unify(v1, v2, substitution)
}
(ShellType::Union(types), other) | (other, ShellType::Union(types)) => {
if types.iter().any(|t| t.is_compatible(other)) {
Ok(())
} else {
Err(TypeErrorKind::IncompatibleTypes)
}
}
_ => Err(TypeErrorKind::IncompatibleTypes),
}
}
#[allow(clippy::only_used_in_recursion)]
fn apply_substitution(
&self,
shell_type: &ShellType,
substitution: &HashMap<u32, ShellType>,
) -> ShellType {
match shell_type {
ShellType::TypeVar(id) => substitution
.get(id)
.cloned()
.unwrap_or_else(|| shell_type.clone()),
ShellType::Array(inner) => {
ShellType::Array(Box::new(self.apply_substitution(inner, substitution)))
}
ShellType::AssocArray { key, value } => ShellType::AssocArray {
key: Box::new(self.apply_substitution(key, substitution)),
value: Box::new(self.apply_substitution(value, substitution)),
},
ShellType::Union(types) => ShellType::Union(
types
.iter()
.map(|t| self.apply_substitution(t, substitution))
.collect(),
),
_ => shell_type.clone(),
}
}
pub fn validate_contracts(&self) -> Vec<ContractViolation> {
let mut violations = Vec::new();
for contract in &self.active_contracts {
if let Some(violation) = self.check_contract(contract) {
violations.push(violation);
}
}
violations
}
fn check_contract(&self, contract: &Contract) -> Option<ContractViolation> {
match &contract.condition {
ContractCondition::TypeConstraint { var, expected_type } => {
if let Some(actual_type) = self.type_env.get(var) {
if !actual_type.is_compatible(expected_type) {
return Some(ContractViolation {
contract: contract.clone(),
reason: format!(
"Variable '{}' has type {} but expected {}",
var,
actual_type.display(),
expected_type.display()
),
});
}
}
}
ContractCondition::NonNull { var } => {
if !self.type_env.contains_key(var) {
return Some(ContractViolation {
contract: contract.clone(),
reason: format!("Variable '{var}' is not defined"),
});
}
}
_ => {
}
}
None
}
pub fn get_variable_type(&self, var_name: &str) -> Option<&ShellType> {
self.type_env.get(var_name)
}
pub fn register_function(&mut self, signature: FunctionSignature) {
self.function_sigs.insert(signature.name.clone(), signature);
}
}
#[derive(Debug, Clone)]
pub enum TypeContext {
Assignment {
value_type: ShellType,
},
FunctionCall {
function: String,
param_index: usize,
},
Arithmetic,
}
impl TypeContext {
pub fn location(&self) -> Span {
Span::new(BytePos(0), BytePos(0))
}
}
impl TypeInferenceEngine {
pub fn fresh_type_var(&mut self) -> ShellType {
let id = self.next_type_var;
self.next_type_var += 1;
ShellType::TypeVar(id)
}
pub fn add_constraint(&mut self, constraint: TypeConstraint) {
self.constraints.push(constraint);
}
}
#[derive(Debug, Clone)]
pub struct TypeError {
pub kind: TypeErrorKind,
pub location: Span,
pub constraint_reason: ConstraintReason,
}
#[derive(Debug, Clone)]
pub enum TypeErrorKind {
IncompatibleTypes,
UndefinedVariable(String),
UndefinedFunction(String),
ArityMismatch { expected: usize, actual: usize },
}
#[derive(Debug, Clone)]
pub struct ContractViolation {
pub contract: Contract,
pub reason: String,
}
impl Default for ContractSystem {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "contract_tests_contract_sys.rs"]
mod tests_extracted;