implica 0.3.3__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of implica might be problematic. Click here for more details.
- {implica-0.3.3 → implica-0.4.0}/PKG-INFO +423 -1
- {implica-0.3.3 → implica-0.4.0}/README.md +422 -0
- {implica-0.3.3 → implica-0.4.0}/pyproject.toml +1 -1
- {implica-0.3.3 → implica-0.4.0}/src/implica/__init__.py +2 -1
- implica-0.4.0/src/implica/core/__init__.py +14 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/core/types.py +44 -0
- implica-0.4.0/src/implica/utils/__init__.py +7 -0
- implica-0.4.0/src/implica/utils/parsing.py +159 -0
- implica-0.3.3/src/implica/core/__init__.py +0 -4
- {implica-0.3.3 → implica-0.4.0}/LICENSE +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/core/combinator.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/graph/__init__.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/graph/connection.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/graph/elements.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/graph/graph.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/__init__.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/add_edge.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/add_many_edges.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/add_many_nodes.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/add_node.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/base.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/remove_edge.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/remove_many_edges.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/remove_many_nodes.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/remove_node.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/try_add_edge.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/try_add_many_edges.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/try_add_many_nodes.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/try_add_node.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/try_remove_edge.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/try_remove_many_edges.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/try_remove_many_nodes.py +0 -0
- {implica-0.3.3 → implica-0.4.0}/src/implica/mutations/try_remove_node.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: implica
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A package for working with graphs representing minimal implicational logic models
|
|
5
5
|
Author: Carlos Fernandez
|
|
6
6
|
Author-email: carlos.ferlo@outlook.com
|
|
@@ -24,6 +24,7 @@ A Python package for working with graphs representing minimal implicational logi
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
26
26
|
- 🎯 **Type System**: Build complex type expressions using variables and applications (function types)
|
|
27
|
+
- 🔍 **Variable Extraction**: Recursively extract all variables from any type expression
|
|
27
28
|
- 🧩 **Combinators**: Work with S and K combinators from combinatory logic
|
|
28
29
|
- 📊 **Graph Structure**: Represent type transformations as nodes and edges in a directed graph
|
|
29
30
|
- 🔄 **Transactional Operations**: Safely modify graphs with automatic rollback on failure
|
|
@@ -106,6 +107,129 @@ print(complex_function) # Output: (A -> B) -> C
|
|
|
106
107
|
print(nested_function) # Output: A -> B -> C
|
|
107
108
|
```
|
|
108
109
|
|
|
110
|
+
#### Parsing Types from Strings
|
|
111
|
+
|
|
112
|
+
You can also create types by parsing string representations using `type_from_string()`:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import implica as imp
|
|
116
|
+
|
|
117
|
+
# Parse simple variables
|
|
118
|
+
A = imp.type_from_string("A")
|
|
119
|
+
print(A) # Output: A
|
|
120
|
+
|
|
121
|
+
# Parse function types
|
|
122
|
+
func = imp.type_from_string("A -> B")
|
|
123
|
+
print(func) # Output: A -> B
|
|
124
|
+
|
|
125
|
+
# Parse nested types with right-associativity
|
|
126
|
+
nested = imp.type_from_string("A -> B -> C")
|
|
127
|
+
print(nested) # Output: A -> B -> C
|
|
128
|
+
# This is equivalent to: A -> (B -> C)
|
|
129
|
+
|
|
130
|
+
# Parse with explicit parentheses
|
|
131
|
+
left_assoc = imp.type_from_string("(A -> B) -> C")
|
|
132
|
+
print(left_assoc) # Output: (A -> B) -> C
|
|
133
|
+
|
|
134
|
+
# Parse complex nested types
|
|
135
|
+
complex = imp.type_from_string("((A -> B) -> C) -> (D -> E)")
|
|
136
|
+
print(complex) # Output: ((A -> B) -> C) -> D -> E
|
|
137
|
+
|
|
138
|
+
# Multi-character variable names
|
|
139
|
+
person_func = imp.type_from_string("Person -> String")
|
|
140
|
+
print(person_func) # Output: Person -> String
|
|
141
|
+
|
|
142
|
+
# Variables with numbers and underscores
|
|
143
|
+
data_type = imp.type_from_string("data_1 -> result_2")
|
|
144
|
+
print(data_type) # Output: data_1 -> result_2
|
|
145
|
+
|
|
146
|
+
# Unicode characters are supported
|
|
147
|
+
greek = imp.type_from_string("α -> β -> γ")
|
|
148
|
+
print(greek) # Output: α -> β -> γ
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Parsing rules:**
|
|
152
|
+
|
|
153
|
+
- The arrow operator `->` is **right-associative**: `A -> B -> C` is parsed as `A -> (B -> C)`
|
|
154
|
+
- Use parentheses to override associativity: `(A -> B) -> C`
|
|
155
|
+
- Variable names can contain letters, numbers, and underscores
|
|
156
|
+
- Whitespace is ignored: `A->B` and `A -> B` are equivalent
|
|
157
|
+
- Unicode characters in variable names are supported
|
|
158
|
+
|
|
159
|
+
**Error handling:**
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
import implica as imp
|
|
163
|
+
|
|
164
|
+
# Empty string raises ValueError
|
|
165
|
+
try:
|
|
166
|
+
imp.type_from_string("")
|
|
167
|
+
except ValueError as e:
|
|
168
|
+
print(f"Error: {e}") # Error: Type string cannot be empty
|
|
169
|
+
|
|
170
|
+
# Mismatched parentheses
|
|
171
|
+
try:
|
|
172
|
+
imp.type_from_string("(A -> B")
|
|
173
|
+
except ValueError as e:
|
|
174
|
+
print(f"Error: {e}") # Error: Mismatched parentheses
|
|
175
|
+
|
|
176
|
+
# Invalid characters
|
|
177
|
+
try:
|
|
178
|
+
imp.type_from_string("A @ B")
|
|
179
|
+
except ValueError as e:
|
|
180
|
+
print(f"Error: {e}") # Error: Invalid character '@' at position 2
|
|
181
|
+
|
|
182
|
+
# Missing operand
|
|
183
|
+
try:
|
|
184
|
+
imp.type_from_string("A ->")
|
|
185
|
+
except ValueError as e:
|
|
186
|
+
print(f"Error: {e}") # Error: Unexpected end of input
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Use cases:**
|
|
190
|
+
|
|
191
|
+
- Parse type expressions from configuration files or user input
|
|
192
|
+
- Create types dynamically from strings
|
|
193
|
+
- Simplify type construction with readable syntax
|
|
194
|
+
- Testing and debugging with human-readable type representations
|
|
195
|
+
|
|
196
|
+
#### Extracting Variables from Types
|
|
197
|
+
|
|
198
|
+
Every type has a `variables` property that returns a list of all variables contained in that type. This is computed recursively and may include duplicate variables if they appear multiple times:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import implica as imp
|
|
202
|
+
|
|
203
|
+
# Simple variable returns itself
|
|
204
|
+
A = imp.var("A")
|
|
205
|
+
print(A.variables) # Output: [Variable(name='A')]
|
|
206
|
+
|
|
207
|
+
# Application returns all variables from both input and output
|
|
208
|
+
B = imp.var("B")
|
|
209
|
+
func = imp.app(A, B)
|
|
210
|
+
print([v.name for v in func.variables]) # Output: ['A', 'B']
|
|
211
|
+
|
|
212
|
+
# Nested types return all variables recursively
|
|
213
|
+
C = imp.var("C")
|
|
214
|
+
nested = imp.app(imp.app(A, B), C) # (A -> B) -> C
|
|
215
|
+
print([v.name for v in nested.variables]) # Output: ['A', 'B', 'C']
|
|
216
|
+
|
|
217
|
+
# Duplicates are included
|
|
218
|
+
same_var = imp.app(A, A) # A -> A
|
|
219
|
+
print([v.name for v in same_var.variables]) # Output: ['A', 'A']
|
|
220
|
+
|
|
221
|
+
# Complex example with duplicates
|
|
222
|
+
complex = imp.app(imp.app(A, B), A) # (A -> B) -> A
|
|
223
|
+
print([v.name for v in complex.variables]) # Output: ['A', 'B', 'A']
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Use cases:**
|
|
227
|
+
|
|
228
|
+
- Analyze which variables are used in a complex type expression
|
|
229
|
+
- Count occurrences of specific variables in a type
|
|
230
|
+
- Validate that certain variables are present or absent
|
|
231
|
+
- Generate variable lists for quantification or substitution operations
|
|
232
|
+
|
|
109
233
|
### Combinators
|
|
110
234
|
|
|
111
235
|
Combinators represent transformations between types. The library includes the S and K combinators from combinatory logic:
|
|
@@ -800,6 +924,249 @@ ensure_graph_state(
|
|
|
800
924
|
print(f"Final state: {graph.node_count()} nodes") # Output: 2 (X and Y)
|
|
801
925
|
```
|
|
802
926
|
|
|
927
|
+
### Example 10: Analyzing Type Variables
|
|
928
|
+
|
|
929
|
+
Extract and analyze variables from complex type expressions:
|
|
930
|
+
|
|
931
|
+
```python
|
|
932
|
+
import implica as imp
|
|
933
|
+
|
|
934
|
+
# Create a complex type expression
|
|
935
|
+
A = imp.var("A")
|
|
936
|
+
B = imp.var("B")
|
|
937
|
+
C = imp.var("C")
|
|
938
|
+
|
|
939
|
+
# ((A -> B) -> C) -> (A -> B)
|
|
940
|
+
inner = imp.app(A, B)
|
|
941
|
+
middle = imp.app(inner, C)
|
|
942
|
+
complex_type = imp.app(middle, inner)
|
|
943
|
+
|
|
944
|
+
print(f"Type: {complex_type}")
|
|
945
|
+
# Output: ((A -> B) -> C) -> A -> B
|
|
946
|
+
|
|
947
|
+
# Get all variables (including duplicates)
|
|
948
|
+
variables = complex_type.variables
|
|
949
|
+
print(f"All variables: {[v.name for v in variables]}")
|
|
950
|
+
# Output: ['A', 'B', 'C', 'A', 'B']
|
|
951
|
+
|
|
952
|
+
# Count unique variables
|
|
953
|
+
unique_vars = {v.name for v in variables}
|
|
954
|
+
print(f"Unique variables: {unique_vars}")
|
|
955
|
+
# Output: {'A', 'B', 'C'}
|
|
956
|
+
|
|
957
|
+
# Count occurrences
|
|
958
|
+
from collections import Counter
|
|
959
|
+
var_counts = Counter(v.name for v in variables)
|
|
960
|
+
print(f"Variable counts: {dict(var_counts)}")
|
|
961
|
+
# Output: {'A': 2, 'B': 2, 'C': 1}
|
|
962
|
+
|
|
963
|
+
# Check if a specific variable is used
|
|
964
|
+
def uses_variable(type_expr, var_name):
|
|
965
|
+
"""Check if a type expression uses a specific variable."""
|
|
966
|
+
return any(v.name == var_name for v in type_expr.variables)
|
|
967
|
+
|
|
968
|
+
print(f"Uses A: {uses_variable(complex_type, 'A')}") # Output: True
|
|
969
|
+
print(f"Uses D: {uses_variable(complex_type, 'D')}") # Output: False
|
|
970
|
+
|
|
971
|
+
# Find all types in a graph that use a specific variable
|
|
972
|
+
graph = imp.Graph()
|
|
973
|
+
|
|
974
|
+
# Create various types
|
|
975
|
+
types_to_add = [
|
|
976
|
+
imp.var("X"),
|
|
977
|
+
imp.var("Y"),
|
|
978
|
+
imp.app(A, B),
|
|
979
|
+
imp.app(B, C),
|
|
980
|
+
imp.app(A, imp.app(B, C)),
|
|
981
|
+
]
|
|
982
|
+
|
|
983
|
+
with graph.connect() as conn:
|
|
984
|
+
for t in types_to_add:
|
|
985
|
+
conn.add_node(imp.node(t))
|
|
986
|
+
|
|
987
|
+
# Find all nodes containing variable "B"
|
|
988
|
+
nodes_with_B = [
|
|
989
|
+
n for n in graph.nodes()
|
|
990
|
+
if any(v.name == "B" for v in n.type.variables)
|
|
991
|
+
]
|
|
992
|
+
|
|
993
|
+
print(f"\nNodes containing variable B:")
|
|
994
|
+
for n in nodes_with_B:
|
|
995
|
+
print(f" - {n.type}")
|
|
996
|
+
# Output:
|
|
997
|
+
# - A -> B
|
|
998
|
+
# - B -> C
|
|
999
|
+
# - A -> B -> C
|
|
1000
|
+
|
|
1001
|
+
# Calculate the "complexity" of a type by counting its variables
|
|
1002
|
+
def type_complexity(type_expr):
|
|
1003
|
+
"""Calculate complexity as the total number of variables."""
|
|
1004
|
+
return len(type_expr.variables)
|
|
1005
|
+
|
|
1006
|
+
print(f"\nType complexities:")
|
|
1007
|
+
for n in graph.nodes():
|
|
1008
|
+
print(f" {n.type}: {type_complexity(n.type)}")
|
|
1009
|
+
# Output:
|
|
1010
|
+
# X: 1
|
|
1011
|
+
# Y: 1
|
|
1012
|
+
# A -> B: 2
|
|
1013
|
+
# B -> C: 2
|
|
1014
|
+
# A -> B -> C: 3
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### Example 11: Using type_from_string for Dynamic Type Creation
|
|
1018
|
+
|
|
1019
|
+
Parse types from strings for flexible, dynamic type construction:
|
|
1020
|
+
|
|
1021
|
+
```python
|
|
1022
|
+
import implica as imp
|
|
1023
|
+
|
|
1024
|
+
# Create a graph
|
|
1025
|
+
graph = imp.Graph()
|
|
1026
|
+
|
|
1027
|
+
# Define type expressions as strings (e.g., from a config file or user input)
|
|
1028
|
+
type_strings = [
|
|
1029
|
+
"Person",
|
|
1030
|
+
"String",
|
|
1031
|
+
"Int",
|
|
1032
|
+
"Person -> String", # Get name
|
|
1033
|
+
"Person -> Int", # Get age
|
|
1034
|
+
"(Person -> String) -> (Person -> Int) -> Person", # Complex transformation
|
|
1035
|
+
]
|
|
1036
|
+
|
|
1037
|
+
# Parse and add all types to the graph
|
|
1038
|
+
nodes = []
|
|
1039
|
+
for type_str in type_strings:
|
|
1040
|
+
parsed_type = imp.type_from_string(type_str)
|
|
1041
|
+
node = imp.node(parsed_type)
|
|
1042
|
+
nodes.append(node)
|
|
1043
|
+
|
|
1044
|
+
with graph.connect() as conn:
|
|
1045
|
+
conn.add_many_nodes(nodes)
|
|
1046
|
+
|
|
1047
|
+
print(f"Created graph with {graph.node_count()} nodes from string definitions")
|
|
1048
|
+
# Output: Created graph with 6 nodes from string definitions
|
|
1049
|
+
|
|
1050
|
+
# You can also build a type library from a configuration
|
|
1051
|
+
type_library = {
|
|
1052
|
+
"identity": "A -> A",
|
|
1053
|
+
"const": "A -> B -> A",
|
|
1054
|
+
"compose": "(B -> C) -> (A -> B) -> A -> C",
|
|
1055
|
+
"apply": "(A -> B) -> A -> B",
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
print("\n=== Type Library ===")
|
|
1059
|
+
for name, type_str in type_library.items():
|
|
1060
|
+
parsed = imp.type_from_string(type_str)
|
|
1061
|
+
print(f"{name:10} : {parsed}")
|
|
1062
|
+
|
|
1063
|
+
# Output:
|
|
1064
|
+
# identity : A -> A
|
|
1065
|
+
# const : A -> B -> A
|
|
1066
|
+
# compose : (B -> C) -> (A -> B) -> A -> C
|
|
1067
|
+
# apply : (A -> B) -> A -> B
|
|
1068
|
+
|
|
1069
|
+
# Validate type strings from user input
|
|
1070
|
+
def validate_and_parse_type(user_input: str) -> tuple[bool, str, object]:
|
|
1071
|
+
"""
|
|
1072
|
+
Validate and parse a type string from user input.
|
|
1073
|
+
Returns (is_valid, message, parsed_type)
|
|
1074
|
+
"""
|
|
1075
|
+
try:
|
|
1076
|
+
parsed = imp.type_from_string(user_input)
|
|
1077
|
+
return True, f"Valid type: {parsed}", parsed
|
|
1078
|
+
except ValueError as e:
|
|
1079
|
+
return False, f"Invalid type: {e}", None
|
|
1080
|
+
|
|
1081
|
+
# Test validation
|
|
1082
|
+
test_inputs = [
|
|
1083
|
+
"A -> B",
|
|
1084
|
+
"(A -> B) -> C",
|
|
1085
|
+
"A ->", # Invalid
|
|
1086
|
+
"Person -> (Address -> String)",
|
|
1087
|
+
"A @ B", # Invalid
|
|
1088
|
+
]
|
|
1089
|
+
|
|
1090
|
+
print("\n=== Type Validation ===")
|
|
1091
|
+
for test in test_inputs:
|
|
1092
|
+
is_valid, message, parsed = validate_and_parse_type(test)
|
|
1093
|
+
status = "✓" if is_valid else "✗"
|
|
1094
|
+
print(f"{status} '{test:30}' : {message}")
|
|
1095
|
+
|
|
1096
|
+
# Output:
|
|
1097
|
+
# ✓ 'A -> B' : Valid type: A -> B
|
|
1098
|
+
# ✓ '(A -> B) -> C' : Valid type: (A -> B) -> C
|
|
1099
|
+
# ✗ 'A ->' : Invalid type: Unexpected end of input
|
|
1100
|
+
# ✓ 'Person -> (Address -> String)' : Valid type: Person -> Address -> String
|
|
1101
|
+
# ✗ 'A @ B' : Invalid type: Invalid character '@' at position 2
|
|
1102
|
+
|
|
1103
|
+
# Build a type inference system
|
|
1104
|
+
def infer_result_type(func_type_str: str, input_type_str: str) -> str:
|
|
1105
|
+
"""
|
|
1106
|
+
Given a function type and an input type, infer the result type.
|
|
1107
|
+
Returns the result type as a string, or an error message.
|
|
1108
|
+
"""
|
|
1109
|
+
try:
|
|
1110
|
+
func_type = imp.type_from_string(func_type_str)
|
|
1111
|
+
input_type = imp.type_from_string(input_type_str)
|
|
1112
|
+
|
|
1113
|
+
# Check if func_type is an application
|
|
1114
|
+
if not isinstance(func_type, imp.Application):
|
|
1115
|
+
return f"Error: {func_type_str} is not a function type"
|
|
1116
|
+
|
|
1117
|
+
# Check if input matches function's input type
|
|
1118
|
+
if func_type.input_type.uid != input_type.uid:
|
|
1119
|
+
return f"Error: Type mismatch. Expected {func_type.input_type}, got {input_type}"
|
|
1120
|
+
|
|
1121
|
+
return str(func_type.output_type)
|
|
1122
|
+
except ValueError as e:
|
|
1123
|
+
return f"Error: {e}"
|
|
1124
|
+
|
|
1125
|
+
# Test type inference
|
|
1126
|
+
print("\n=== Type Inference ===")
|
|
1127
|
+
print(f"Apply 'A -> B' to 'A': {infer_result_type('A -> B', 'A')}")
|
|
1128
|
+
# Output: B
|
|
1129
|
+
|
|
1130
|
+
print(f"Apply 'Person -> String' to 'Person': {infer_result_type('Person -> String', 'Person')}")
|
|
1131
|
+
# Output: String
|
|
1132
|
+
|
|
1133
|
+
print(f"Apply 'A -> B -> C' to 'A': {infer_result_type('A -> B -> C', 'A')}")
|
|
1134
|
+
# Output: B -> C
|
|
1135
|
+
|
|
1136
|
+
print(f"Apply 'A -> B' to 'C': {infer_result_type('A -> B', 'C')}")
|
|
1137
|
+
# Output: Error: Type mismatch. Expected A, got C
|
|
1138
|
+
|
|
1139
|
+
# Parse types from a domain-specific language
|
|
1140
|
+
dsl_program = """
|
|
1141
|
+
define Input
|
|
1142
|
+
define Output
|
|
1143
|
+
define Processor = Input -> Output
|
|
1144
|
+
define Chain = Processor -> Processor -> Processor
|
|
1145
|
+
"""
|
|
1146
|
+
|
|
1147
|
+
print("\n=== Parsing DSL ===")
|
|
1148
|
+
for line in dsl_program.strip().split('\n'):
|
|
1149
|
+
line = line.strip()
|
|
1150
|
+
if line.startswith("define "):
|
|
1151
|
+
parts = line[7:].split(" = ")
|
|
1152
|
+
type_name = parts[0].strip()
|
|
1153
|
+
|
|
1154
|
+
if len(parts) == 1:
|
|
1155
|
+
# Simple type definition
|
|
1156
|
+
print(f"{type_name}: <primitive type>")
|
|
1157
|
+
else:
|
|
1158
|
+
# Complex type definition
|
|
1159
|
+
type_expr = parts[1].strip()
|
|
1160
|
+
parsed = imp.type_from_string(type_expr)
|
|
1161
|
+
print(f"{type_name}: {parsed}")
|
|
1162
|
+
|
|
1163
|
+
# Output:
|
|
1164
|
+
# Input: <primitive type>
|
|
1165
|
+
# Output: <primitive type>
|
|
1166
|
+
# Processor: Input -> Output
|
|
1167
|
+
# Chain: Processor -> Processor -> Processor
|
|
1168
|
+
```
|
|
1169
|
+
|
|
803
1170
|
## API Reference
|
|
804
1171
|
|
|
805
1172
|
### Core Module (`implica.core`)
|
|
@@ -808,8 +1175,16 @@ print(f"Final state: {graph.node_count()} nodes") # Output: 2 (X and Y)
|
|
|
808
1175
|
|
|
809
1176
|
- `var(name: str) -> Variable`: Create a type variable
|
|
810
1177
|
- `app(input_type: BaseType, output_type: BaseType) -> Application`: Create a function type
|
|
1178
|
+
- `type_from_string(type_str: str) -> BaseType`: Parse a type from a string representation
|
|
811
1179
|
- `Variable`: Atomic type variable
|
|
1180
|
+
- `name: str`: The name of the variable
|
|
1181
|
+
- `uid: str`: Unique identifier (SHA256 hash)
|
|
1182
|
+
- `variables: list[Variable]`: Returns `[self]`
|
|
812
1183
|
- `Application`: Function application type
|
|
1184
|
+
- `input_type: BaseType`: Input type of the function
|
|
1185
|
+
- `output_type: BaseType`: Output type of the function
|
|
1186
|
+
- `uid: str`: Unique identifier (SHA256 hash)
|
|
1187
|
+
- `variables: list[Variable]`: Returns all variables from input and output types (may include duplicates)
|
|
813
1188
|
|
|
814
1189
|
**Combinators:**
|
|
815
1190
|
|
|
@@ -875,6 +1250,53 @@ print(f"Final state: {graph.node_count()} nodes") # Output: 2 (X and Y)
|
|
|
875
1250
|
- `TryRemoveNode(node_uid)`: Remove a node or do nothing if it doesn't exist
|
|
876
1251
|
- `TryRemoveEdge(edge_uid)`: Remove an edge or do nothing if it doesn't exist
|
|
877
1252
|
|
|
1253
|
+
### Utilities Module (`implica.utils`)
|
|
1254
|
+
|
|
1255
|
+
**Parsing Functions:**
|
|
1256
|
+
|
|
1257
|
+
- `tokenize(type_str: str) -> list[str]`: Tokenize a type string into tokens
|
|
1258
|
+
- Returns a list of tokens: variable names, `'('`, `')'`, and `'->'`
|
|
1259
|
+
- Raises `ValueError` if invalid characters are found
|
|
1260
|
+
- `parse_type(tokens: list[str]) -> tuple[BaseType, list[str]]`: Parse tokens into a type
|
|
1261
|
+
- Uses recursive descent parsing with right-associativity for arrows
|
|
1262
|
+
- Returns the parsed type and any remaining tokens
|
|
1263
|
+
- Raises `ValueError` if tokens cannot be parsed
|
|
1264
|
+
- `parse_primary(tokens: list[str]) -> tuple[BaseType, list[str]]`: Parse a primary type
|
|
1265
|
+
- Handles variables and parenthesized types
|
|
1266
|
+
- Returns the parsed type and remaining tokens
|
|
1267
|
+
- Raises `ValueError` if tokens cannot be parsed
|
|
1268
|
+
|
|
1269
|
+
**Note:** These functions are primarily used internally by `type_from_string()`, but are exposed for advanced use cases where you need direct control over the parsing process.
|
|
1270
|
+
|
|
1271
|
+
**Example:**
|
|
1272
|
+
|
|
1273
|
+
```python
|
|
1274
|
+
from implica.utils import tokenize, parse_type
|
|
1275
|
+
|
|
1276
|
+
# Tokenize a type string
|
|
1277
|
+
tokens = tokenize("(A -> B) -> C")
|
|
1278
|
+
print(tokens) # Output: ['(', 'A', '->', 'B', ')', '->', 'C']
|
|
1279
|
+
|
|
1280
|
+
# Parse the tokens
|
|
1281
|
+
type_result, remaining = parse_type(tokens)
|
|
1282
|
+
print(type_result) # Output: (A -> B) -> C
|
|
1283
|
+
print(remaining) # Output: []
|
|
1284
|
+
|
|
1285
|
+
# Custom parsing workflow
|
|
1286
|
+
def parse_and_validate(type_str: str) -> bool:
|
|
1287
|
+
"""Check if a string is a valid type expression."""
|
|
1288
|
+
try:
|
|
1289
|
+
tokens = tokenize(type_str)
|
|
1290
|
+
result, remaining = parse_type(tokens)
|
|
1291
|
+
return len(remaining) == 0 # Valid if no tokens remain
|
|
1292
|
+
except ValueError:
|
|
1293
|
+
return False
|
|
1294
|
+
|
|
1295
|
+
print(parse_and_validate("A -> B")) # Output: True
|
|
1296
|
+
print(parse_and_validate("A ->")) # Output: False
|
|
1297
|
+
print(parse_and_validate("(A -> B) -> C")) # Output: True
|
|
1298
|
+
```
|
|
1299
|
+
|
|
878
1300
|
## Development
|
|
879
1301
|
|
|
880
1302
|
### Setup
|
|
@@ -10,6 +10,7 @@ A Python package for working with graphs representing minimal implicational logi
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
12
|
- 🎯 **Type System**: Build complex type expressions using variables and applications (function types)
|
|
13
|
+
- 🔍 **Variable Extraction**: Recursively extract all variables from any type expression
|
|
13
14
|
- 🧩 **Combinators**: Work with S and K combinators from combinatory logic
|
|
14
15
|
- 📊 **Graph Structure**: Represent type transformations as nodes and edges in a directed graph
|
|
15
16
|
- 🔄 **Transactional Operations**: Safely modify graphs with automatic rollback on failure
|
|
@@ -92,6 +93,129 @@ print(complex_function) # Output: (A -> B) -> C
|
|
|
92
93
|
print(nested_function) # Output: A -> B -> C
|
|
93
94
|
```
|
|
94
95
|
|
|
96
|
+
#### Parsing Types from Strings
|
|
97
|
+
|
|
98
|
+
You can also create types by parsing string representations using `type_from_string()`:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import implica as imp
|
|
102
|
+
|
|
103
|
+
# Parse simple variables
|
|
104
|
+
A = imp.type_from_string("A")
|
|
105
|
+
print(A) # Output: A
|
|
106
|
+
|
|
107
|
+
# Parse function types
|
|
108
|
+
func = imp.type_from_string("A -> B")
|
|
109
|
+
print(func) # Output: A -> B
|
|
110
|
+
|
|
111
|
+
# Parse nested types with right-associativity
|
|
112
|
+
nested = imp.type_from_string("A -> B -> C")
|
|
113
|
+
print(nested) # Output: A -> B -> C
|
|
114
|
+
# This is equivalent to: A -> (B -> C)
|
|
115
|
+
|
|
116
|
+
# Parse with explicit parentheses
|
|
117
|
+
left_assoc = imp.type_from_string("(A -> B) -> C")
|
|
118
|
+
print(left_assoc) # Output: (A -> B) -> C
|
|
119
|
+
|
|
120
|
+
# Parse complex nested types
|
|
121
|
+
complex = imp.type_from_string("((A -> B) -> C) -> (D -> E)")
|
|
122
|
+
print(complex) # Output: ((A -> B) -> C) -> D -> E
|
|
123
|
+
|
|
124
|
+
# Multi-character variable names
|
|
125
|
+
person_func = imp.type_from_string("Person -> String")
|
|
126
|
+
print(person_func) # Output: Person -> String
|
|
127
|
+
|
|
128
|
+
# Variables with numbers and underscores
|
|
129
|
+
data_type = imp.type_from_string("data_1 -> result_2")
|
|
130
|
+
print(data_type) # Output: data_1 -> result_2
|
|
131
|
+
|
|
132
|
+
# Unicode characters are supported
|
|
133
|
+
greek = imp.type_from_string("α -> β -> γ")
|
|
134
|
+
print(greek) # Output: α -> β -> γ
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Parsing rules:**
|
|
138
|
+
|
|
139
|
+
- The arrow operator `->` is **right-associative**: `A -> B -> C` is parsed as `A -> (B -> C)`
|
|
140
|
+
- Use parentheses to override associativity: `(A -> B) -> C`
|
|
141
|
+
- Variable names can contain letters, numbers, and underscores
|
|
142
|
+
- Whitespace is ignored: `A->B` and `A -> B` are equivalent
|
|
143
|
+
- Unicode characters in variable names are supported
|
|
144
|
+
|
|
145
|
+
**Error handling:**
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
import implica as imp
|
|
149
|
+
|
|
150
|
+
# Empty string raises ValueError
|
|
151
|
+
try:
|
|
152
|
+
imp.type_from_string("")
|
|
153
|
+
except ValueError as e:
|
|
154
|
+
print(f"Error: {e}") # Error: Type string cannot be empty
|
|
155
|
+
|
|
156
|
+
# Mismatched parentheses
|
|
157
|
+
try:
|
|
158
|
+
imp.type_from_string("(A -> B")
|
|
159
|
+
except ValueError as e:
|
|
160
|
+
print(f"Error: {e}") # Error: Mismatched parentheses
|
|
161
|
+
|
|
162
|
+
# Invalid characters
|
|
163
|
+
try:
|
|
164
|
+
imp.type_from_string("A @ B")
|
|
165
|
+
except ValueError as e:
|
|
166
|
+
print(f"Error: {e}") # Error: Invalid character '@' at position 2
|
|
167
|
+
|
|
168
|
+
# Missing operand
|
|
169
|
+
try:
|
|
170
|
+
imp.type_from_string("A ->")
|
|
171
|
+
except ValueError as e:
|
|
172
|
+
print(f"Error: {e}") # Error: Unexpected end of input
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Use cases:**
|
|
176
|
+
|
|
177
|
+
- Parse type expressions from configuration files or user input
|
|
178
|
+
- Create types dynamically from strings
|
|
179
|
+
- Simplify type construction with readable syntax
|
|
180
|
+
- Testing and debugging with human-readable type representations
|
|
181
|
+
|
|
182
|
+
#### Extracting Variables from Types
|
|
183
|
+
|
|
184
|
+
Every type has a `variables` property that returns a list of all variables contained in that type. This is computed recursively and may include duplicate variables if they appear multiple times:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
import implica as imp
|
|
188
|
+
|
|
189
|
+
# Simple variable returns itself
|
|
190
|
+
A = imp.var("A")
|
|
191
|
+
print(A.variables) # Output: [Variable(name='A')]
|
|
192
|
+
|
|
193
|
+
# Application returns all variables from both input and output
|
|
194
|
+
B = imp.var("B")
|
|
195
|
+
func = imp.app(A, B)
|
|
196
|
+
print([v.name for v in func.variables]) # Output: ['A', 'B']
|
|
197
|
+
|
|
198
|
+
# Nested types return all variables recursively
|
|
199
|
+
C = imp.var("C")
|
|
200
|
+
nested = imp.app(imp.app(A, B), C) # (A -> B) -> C
|
|
201
|
+
print([v.name for v in nested.variables]) # Output: ['A', 'B', 'C']
|
|
202
|
+
|
|
203
|
+
# Duplicates are included
|
|
204
|
+
same_var = imp.app(A, A) # A -> A
|
|
205
|
+
print([v.name for v in same_var.variables]) # Output: ['A', 'A']
|
|
206
|
+
|
|
207
|
+
# Complex example with duplicates
|
|
208
|
+
complex = imp.app(imp.app(A, B), A) # (A -> B) -> A
|
|
209
|
+
print([v.name for v in complex.variables]) # Output: ['A', 'B', 'A']
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Use cases:**
|
|
213
|
+
|
|
214
|
+
- Analyze which variables are used in a complex type expression
|
|
215
|
+
- Count occurrences of specific variables in a type
|
|
216
|
+
- Validate that certain variables are present or absent
|
|
217
|
+
- Generate variable lists for quantification or substitution operations
|
|
218
|
+
|
|
95
219
|
### Combinators
|
|
96
220
|
|
|
97
221
|
Combinators represent transformations between types. The library includes the S and K combinators from combinatory logic:
|
|
@@ -786,6 +910,249 @@ ensure_graph_state(
|
|
|
786
910
|
print(f"Final state: {graph.node_count()} nodes") # Output: 2 (X and Y)
|
|
787
911
|
```
|
|
788
912
|
|
|
913
|
+
### Example 10: Analyzing Type Variables
|
|
914
|
+
|
|
915
|
+
Extract and analyze variables from complex type expressions:
|
|
916
|
+
|
|
917
|
+
```python
|
|
918
|
+
import implica as imp
|
|
919
|
+
|
|
920
|
+
# Create a complex type expression
|
|
921
|
+
A = imp.var("A")
|
|
922
|
+
B = imp.var("B")
|
|
923
|
+
C = imp.var("C")
|
|
924
|
+
|
|
925
|
+
# ((A -> B) -> C) -> (A -> B)
|
|
926
|
+
inner = imp.app(A, B)
|
|
927
|
+
middle = imp.app(inner, C)
|
|
928
|
+
complex_type = imp.app(middle, inner)
|
|
929
|
+
|
|
930
|
+
print(f"Type: {complex_type}")
|
|
931
|
+
# Output: ((A -> B) -> C) -> A -> B
|
|
932
|
+
|
|
933
|
+
# Get all variables (including duplicates)
|
|
934
|
+
variables = complex_type.variables
|
|
935
|
+
print(f"All variables: {[v.name for v in variables]}")
|
|
936
|
+
# Output: ['A', 'B', 'C', 'A', 'B']
|
|
937
|
+
|
|
938
|
+
# Count unique variables
|
|
939
|
+
unique_vars = {v.name for v in variables}
|
|
940
|
+
print(f"Unique variables: {unique_vars}")
|
|
941
|
+
# Output: {'A', 'B', 'C'}
|
|
942
|
+
|
|
943
|
+
# Count occurrences
|
|
944
|
+
from collections import Counter
|
|
945
|
+
var_counts = Counter(v.name for v in variables)
|
|
946
|
+
print(f"Variable counts: {dict(var_counts)}")
|
|
947
|
+
# Output: {'A': 2, 'B': 2, 'C': 1}
|
|
948
|
+
|
|
949
|
+
# Check if a specific variable is used
|
|
950
|
+
def uses_variable(type_expr, var_name):
|
|
951
|
+
"""Check if a type expression uses a specific variable."""
|
|
952
|
+
return any(v.name == var_name for v in type_expr.variables)
|
|
953
|
+
|
|
954
|
+
print(f"Uses A: {uses_variable(complex_type, 'A')}") # Output: True
|
|
955
|
+
print(f"Uses D: {uses_variable(complex_type, 'D')}") # Output: False
|
|
956
|
+
|
|
957
|
+
# Find all types in a graph that use a specific variable
|
|
958
|
+
graph = imp.Graph()
|
|
959
|
+
|
|
960
|
+
# Create various types
|
|
961
|
+
types_to_add = [
|
|
962
|
+
imp.var("X"),
|
|
963
|
+
imp.var("Y"),
|
|
964
|
+
imp.app(A, B),
|
|
965
|
+
imp.app(B, C),
|
|
966
|
+
imp.app(A, imp.app(B, C)),
|
|
967
|
+
]
|
|
968
|
+
|
|
969
|
+
with graph.connect() as conn:
|
|
970
|
+
for t in types_to_add:
|
|
971
|
+
conn.add_node(imp.node(t))
|
|
972
|
+
|
|
973
|
+
# Find all nodes containing variable "B"
|
|
974
|
+
nodes_with_B = [
|
|
975
|
+
n for n in graph.nodes()
|
|
976
|
+
if any(v.name == "B" for v in n.type.variables)
|
|
977
|
+
]
|
|
978
|
+
|
|
979
|
+
print(f"\nNodes containing variable B:")
|
|
980
|
+
for n in nodes_with_B:
|
|
981
|
+
print(f" - {n.type}")
|
|
982
|
+
# Output:
|
|
983
|
+
# - A -> B
|
|
984
|
+
# - B -> C
|
|
985
|
+
# - A -> B -> C
|
|
986
|
+
|
|
987
|
+
# Calculate the "complexity" of a type by counting its variables
|
|
988
|
+
def type_complexity(type_expr):
|
|
989
|
+
"""Calculate complexity as the total number of variables."""
|
|
990
|
+
return len(type_expr.variables)
|
|
991
|
+
|
|
992
|
+
print(f"\nType complexities:")
|
|
993
|
+
for n in graph.nodes():
|
|
994
|
+
print(f" {n.type}: {type_complexity(n.type)}")
|
|
995
|
+
# Output:
|
|
996
|
+
# X: 1
|
|
997
|
+
# Y: 1
|
|
998
|
+
# A -> B: 2
|
|
999
|
+
# B -> C: 2
|
|
1000
|
+
# A -> B -> C: 3
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
### Example 11: Using type_from_string for Dynamic Type Creation
|
|
1004
|
+
|
|
1005
|
+
Parse types from strings for flexible, dynamic type construction:
|
|
1006
|
+
|
|
1007
|
+
```python
|
|
1008
|
+
import implica as imp
|
|
1009
|
+
|
|
1010
|
+
# Create a graph
|
|
1011
|
+
graph = imp.Graph()
|
|
1012
|
+
|
|
1013
|
+
# Define type expressions as strings (e.g., from a config file or user input)
|
|
1014
|
+
type_strings = [
|
|
1015
|
+
"Person",
|
|
1016
|
+
"String",
|
|
1017
|
+
"Int",
|
|
1018
|
+
"Person -> String", # Get name
|
|
1019
|
+
"Person -> Int", # Get age
|
|
1020
|
+
"(Person -> String) -> (Person -> Int) -> Person", # Complex transformation
|
|
1021
|
+
]
|
|
1022
|
+
|
|
1023
|
+
# Parse and add all types to the graph
|
|
1024
|
+
nodes = []
|
|
1025
|
+
for type_str in type_strings:
|
|
1026
|
+
parsed_type = imp.type_from_string(type_str)
|
|
1027
|
+
node = imp.node(parsed_type)
|
|
1028
|
+
nodes.append(node)
|
|
1029
|
+
|
|
1030
|
+
with graph.connect() as conn:
|
|
1031
|
+
conn.add_many_nodes(nodes)
|
|
1032
|
+
|
|
1033
|
+
print(f"Created graph with {graph.node_count()} nodes from string definitions")
|
|
1034
|
+
# Output: Created graph with 6 nodes from string definitions
|
|
1035
|
+
|
|
1036
|
+
# You can also build a type library from a configuration
|
|
1037
|
+
type_library = {
|
|
1038
|
+
"identity": "A -> A",
|
|
1039
|
+
"const": "A -> B -> A",
|
|
1040
|
+
"compose": "(B -> C) -> (A -> B) -> A -> C",
|
|
1041
|
+
"apply": "(A -> B) -> A -> B",
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
print("\n=== Type Library ===")
|
|
1045
|
+
for name, type_str in type_library.items():
|
|
1046
|
+
parsed = imp.type_from_string(type_str)
|
|
1047
|
+
print(f"{name:10} : {parsed}")
|
|
1048
|
+
|
|
1049
|
+
# Output:
|
|
1050
|
+
# identity : A -> A
|
|
1051
|
+
# const : A -> B -> A
|
|
1052
|
+
# compose : (B -> C) -> (A -> B) -> A -> C
|
|
1053
|
+
# apply : (A -> B) -> A -> B
|
|
1054
|
+
|
|
1055
|
+
# Validate type strings from user input
|
|
1056
|
+
def validate_and_parse_type(user_input: str) -> tuple[bool, str, object]:
|
|
1057
|
+
"""
|
|
1058
|
+
Validate and parse a type string from user input.
|
|
1059
|
+
Returns (is_valid, message, parsed_type)
|
|
1060
|
+
"""
|
|
1061
|
+
try:
|
|
1062
|
+
parsed = imp.type_from_string(user_input)
|
|
1063
|
+
return True, f"Valid type: {parsed}", parsed
|
|
1064
|
+
except ValueError as e:
|
|
1065
|
+
return False, f"Invalid type: {e}", None
|
|
1066
|
+
|
|
1067
|
+
# Test validation
|
|
1068
|
+
test_inputs = [
|
|
1069
|
+
"A -> B",
|
|
1070
|
+
"(A -> B) -> C",
|
|
1071
|
+
"A ->", # Invalid
|
|
1072
|
+
"Person -> (Address -> String)",
|
|
1073
|
+
"A @ B", # Invalid
|
|
1074
|
+
]
|
|
1075
|
+
|
|
1076
|
+
print("\n=== Type Validation ===")
|
|
1077
|
+
for test in test_inputs:
|
|
1078
|
+
is_valid, message, parsed = validate_and_parse_type(test)
|
|
1079
|
+
status = "✓" if is_valid else "✗"
|
|
1080
|
+
print(f"{status} '{test:30}' : {message}")
|
|
1081
|
+
|
|
1082
|
+
# Output:
|
|
1083
|
+
# ✓ 'A -> B' : Valid type: A -> B
|
|
1084
|
+
# ✓ '(A -> B) -> C' : Valid type: (A -> B) -> C
|
|
1085
|
+
# ✗ 'A ->' : Invalid type: Unexpected end of input
|
|
1086
|
+
# ✓ 'Person -> (Address -> String)' : Valid type: Person -> Address -> String
|
|
1087
|
+
# ✗ 'A @ B' : Invalid type: Invalid character '@' at position 2
|
|
1088
|
+
|
|
1089
|
+
# Build a type inference system
|
|
1090
|
+
def infer_result_type(func_type_str: str, input_type_str: str) -> str:
|
|
1091
|
+
"""
|
|
1092
|
+
Given a function type and an input type, infer the result type.
|
|
1093
|
+
Returns the result type as a string, or an error message.
|
|
1094
|
+
"""
|
|
1095
|
+
try:
|
|
1096
|
+
func_type = imp.type_from_string(func_type_str)
|
|
1097
|
+
input_type = imp.type_from_string(input_type_str)
|
|
1098
|
+
|
|
1099
|
+
# Check if func_type is an application
|
|
1100
|
+
if not isinstance(func_type, imp.Application):
|
|
1101
|
+
return f"Error: {func_type_str} is not a function type"
|
|
1102
|
+
|
|
1103
|
+
# Check if input matches function's input type
|
|
1104
|
+
if func_type.input_type.uid != input_type.uid:
|
|
1105
|
+
return f"Error: Type mismatch. Expected {func_type.input_type}, got {input_type}"
|
|
1106
|
+
|
|
1107
|
+
return str(func_type.output_type)
|
|
1108
|
+
except ValueError as e:
|
|
1109
|
+
return f"Error: {e}"
|
|
1110
|
+
|
|
1111
|
+
# Test type inference
|
|
1112
|
+
print("\n=== Type Inference ===")
|
|
1113
|
+
print(f"Apply 'A -> B' to 'A': {infer_result_type('A -> B', 'A')}")
|
|
1114
|
+
# Output: B
|
|
1115
|
+
|
|
1116
|
+
print(f"Apply 'Person -> String' to 'Person': {infer_result_type('Person -> String', 'Person')}")
|
|
1117
|
+
# Output: String
|
|
1118
|
+
|
|
1119
|
+
print(f"Apply 'A -> B -> C' to 'A': {infer_result_type('A -> B -> C', 'A')}")
|
|
1120
|
+
# Output: B -> C
|
|
1121
|
+
|
|
1122
|
+
print(f"Apply 'A -> B' to 'C': {infer_result_type('A -> B', 'C')}")
|
|
1123
|
+
# Output: Error: Type mismatch. Expected A, got C
|
|
1124
|
+
|
|
1125
|
+
# Parse types from a domain-specific language
|
|
1126
|
+
dsl_program = """
|
|
1127
|
+
define Input
|
|
1128
|
+
define Output
|
|
1129
|
+
define Processor = Input -> Output
|
|
1130
|
+
define Chain = Processor -> Processor -> Processor
|
|
1131
|
+
"""
|
|
1132
|
+
|
|
1133
|
+
print("\n=== Parsing DSL ===")
|
|
1134
|
+
for line in dsl_program.strip().split('\n'):
|
|
1135
|
+
line = line.strip()
|
|
1136
|
+
if line.startswith("define "):
|
|
1137
|
+
parts = line[7:].split(" = ")
|
|
1138
|
+
type_name = parts[0].strip()
|
|
1139
|
+
|
|
1140
|
+
if len(parts) == 1:
|
|
1141
|
+
# Simple type definition
|
|
1142
|
+
print(f"{type_name}: <primitive type>")
|
|
1143
|
+
else:
|
|
1144
|
+
# Complex type definition
|
|
1145
|
+
type_expr = parts[1].strip()
|
|
1146
|
+
parsed = imp.type_from_string(type_expr)
|
|
1147
|
+
print(f"{type_name}: {parsed}")
|
|
1148
|
+
|
|
1149
|
+
# Output:
|
|
1150
|
+
# Input: <primitive type>
|
|
1151
|
+
# Output: <primitive type>
|
|
1152
|
+
# Processor: Input -> Output
|
|
1153
|
+
# Chain: Processor -> Processor -> Processor
|
|
1154
|
+
```
|
|
1155
|
+
|
|
789
1156
|
## API Reference
|
|
790
1157
|
|
|
791
1158
|
### Core Module (`implica.core`)
|
|
@@ -794,8 +1161,16 @@ print(f"Final state: {graph.node_count()} nodes") # Output: 2 (X and Y)
|
|
|
794
1161
|
|
|
795
1162
|
- `var(name: str) -> Variable`: Create a type variable
|
|
796
1163
|
- `app(input_type: BaseType, output_type: BaseType) -> Application`: Create a function type
|
|
1164
|
+
- `type_from_string(type_str: str) -> BaseType`: Parse a type from a string representation
|
|
797
1165
|
- `Variable`: Atomic type variable
|
|
1166
|
+
- `name: str`: The name of the variable
|
|
1167
|
+
- `uid: str`: Unique identifier (SHA256 hash)
|
|
1168
|
+
- `variables: list[Variable]`: Returns `[self]`
|
|
798
1169
|
- `Application`: Function application type
|
|
1170
|
+
- `input_type: BaseType`: Input type of the function
|
|
1171
|
+
- `output_type: BaseType`: Output type of the function
|
|
1172
|
+
- `uid: str`: Unique identifier (SHA256 hash)
|
|
1173
|
+
- `variables: list[Variable]`: Returns all variables from input and output types (may include duplicates)
|
|
799
1174
|
|
|
800
1175
|
**Combinators:**
|
|
801
1176
|
|
|
@@ -861,6 +1236,53 @@ print(f"Final state: {graph.node_count()} nodes") # Output: 2 (X and Y)
|
|
|
861
1236
|
- `TryRemoveNode(node_uid)`: Remove a node or do nothing if it doesn't exist
|
|
862
1237
|
- `TryRemoveEdge(edge_uid)`: Remove an edge or do nothing if it doesn't exist
|
|
863
1238
|
|
|
1239
|
+
### Utilities Module (`implica.utils`)
|
|
1240
|
+
|
|
1241
|
+
**Parsing Functions:**
|
|
1242
|
+
|
|
1243
|
+
- `tokenize(type_str: str) -> list[str]`: Tokenize a type string into tokens
|
|
1244
|
+
- Returns a list of tokens: variable names, `'('`, `')'`, and `'->'`
|
|
1245
|
+
- Raises `ValueError` if invalid characters are found
|
|
1246
|
+
- `parse_type(tokens: list[str]) -> tuple[BaseType, list[str]]`: Parse tokens into a type
|
|
1247
|
+
- Uses recursive descent parsing with right-associativity for arrows
|
|
1248
|
+
- Returns the parsed type and any remaining tokens
|
|
1249
|
+
- Raises `ValueError` if tokens cannot be parsed
|
|
1250
|
+
- `parse_primary(tokens: list[str]) -> tuple[BaseType, list[str]]`: Parse a primary type
|
|
1251
|
+
- Handles variables and parenthesized types
|
|
1252
|
+
- Returns the parsed type and remaining tokens
|
|
1253
|
+
- Raises `ValueError` if tokens cannot be parsed
|
|
1254
|
+
|
|
1255
|
+
**Note:** These functions are primarily used internally by `type_from_string()`, but are exposed for advanced use cases where you need direct control over the parsing process.
|
|
1256
|
+
|
|
1257
|
+
**Example:**
|
|
1258
|
+
|
|
1259
|
+
```python
|
|
1260
|
+
from implica.utils import tokenize, parse_type
|
|
1261
|
+
|
|
1262
|
+
# Tokenize a type string
|
|
1263
|
+
tokens = tokenize("(A -> B) -> C")
|
|
1264
|
+
print(tokens) # Output: ['(', 'A', '->', 'B', ')', '->', 'C']
|
|
1265
|
+
|
|
1266
|
+
# Parse the tokens
|
|
1267
|
+
type_result, remaining = parse_type(tokens)
|
|
1268
|
+
print(type_result) # Output: (A -> B) -> C
|
|
1269
|
+
print(remaining) # Output: []
|
|
1270
|
+
|
|
1271
|
+
# Custom parsing workflow
|
|
1272
|
+
def parse_and_validate(type_str: str) -> bool:
|
|
1273
|
+
"""Check if a string is a valid type expression."""
|
|
1274
|
+
try:
|
|
1275
|
+
tokens = tokenize(type_str)
|
|
1276
|
+
result, remaining = parse_type(tokens)
|
|
1277
|
+
return len(remaining) == 0 # Valid if no tokens remain
|
|
1278
|
+
except ValueError:
|
|
1279
|
+
return False
|
|
1280
|
+
|
|
1281
|
+
print(parse_and_validate("A -> B")) # Output: True
|
|
1282
|
+
print(parse_and_validate("A ->")) # Output: False
|
|
1283
|
+
print(parse_and_validate("(A -> B) -> C")) # Output: True
|
|
1284
|
+
```
|
|
1285
|
+
|
|
864
1286
|
## Development
|
|
865
1287
|
|
|
866
1288
|
### Setup
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .types import BaseType, Variable, Application, var, app, type_from_string
|
|
2
|
+
from .combinator import Combinator, S, K
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"BaseType",
|
|
6
|
+
"Variable",
|
|
7
|
+
"Application",
|
|
8
|
+
"var",
|
|
9
|
+
"app",
|
|
10
|
+
"type_from_string",
|
|
11
|
+
"Combinator",
|
|
12
|
+
"S",
|
|
13
|
+
"K",
|
|
14
|
+
]
|
|
@@ -178,3 +178,47 @@ def app(input_type: BaseType, output_type: BaseType) -> Application:
|
|
|
178
178
|
Application: A new Application instance
|
|
179
179
|
"""
|
|
180
180
|
return Application(input_type=input_type, output_type=output_type)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def type_from_string(type_str: str) -> BaseType:
|
|
184
|
+
"""
|
|
185
|
+
Parse a string representation of a type into a BaseType instance.
|
|
186
|
+
|
|
187
|
+
Supports simple variables (e.g., "A") and implication types (e.g., "A -> B").
|
|
188
|
+
Handles nested types with parentheses (e.g., "(A -> B) -> C").
|
|
189
|
+
The arrow operator (->) is right-associative, so "A -> B -> C" is parsed as "A -> (B -> C)".
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
type_str: String representation of the type
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
BaseType: The parsed type (Variable or Application)
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
ValueError: If the string cannot be parsed as a valid type
|
|
199
|
+
|
|
200
|
+
Examples:
|
|
201
|
+
>>> type_from_string("A")
|
|
202
|
+
Variable(name='A')
|
|
203
|
+
>>> type_from_string("A -> B")
|
|
204
|
+
Application(input_type=Variable(name='A'), output_type=Variable(name='B'))
|
|
205
|
+
>>> type_from_string("(A -> B) -> C")
|
|
206
|
+
Application(input_type=Application(...), output_type=Variable(name='C'))
|
|
207
|
+
"""
|
|
208
|
+
if not type_str or not type_str.strip():
|
|
209
|
+
raise ValueError("Type string cannot be empty")
|
|
210
|
+
|
|
211
|
+
type_str = type_str.strip()
|
|
212
|
+
|
|
213
|
+
# Tokenize the input using the utils module
|
|
214
|
+
from implica.utils import tokenize, parse_type
|
|
215
|
+
|
|
216
|
+
tokens = tokenize(type_str)
|
|
217
|
+
|
|
218
|
+
# Parse the tokens into a type using the utils module
|
|
219
|
+
result, remaining = parse_type(tokens)
|
|
220
|
+
|
|
221
|
+
if remaining:
|
|
222
|
+
raise ValueError(f"Unexpected tokens after parsing: {' '.join(remaining)}")
|
|
223
|
+
|
|
224
|
+
return result
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for parsing type strings.
|
|
3
|
+
|
|
4
|
+
This module contains helper functions for parsing and manipulating types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from implica.core.types import BaseType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def tokenize(type_str: str) -> List[str]:
|
|
14
|
+
"""
|
|
15
|
+
Tokenize a type string into a list of tokens.
|
|
16
|
+
|
|
17
|
+
Tokens include: variable names, '(', ')', and '->'.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
type_str: The type string to tokenize
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List[str]: List of tokens
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If invalid characters are found
|
|
27
|
+
"""
|
|
28
|
+
tokens = []
|
|
29
|
+
i = 0
|
|
30
|
+
while i < len(type_str):
|
|
31
|
+
char = type_str[i]
|
|
32
|
+
|
|
33
|
+
# Skip whitespace
|
|
34
|
+
if char.isspace():
|
|
35
|
+
i += 1
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
# Handle parentheses
|
|
39
|
+
if char in "()":
|
|
40
|
+
tokens.append(char)
|
|
41
|
+
i += 1
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Handle arrow operator
|
|
45
|
+
if char == "-":
|
|
46
|
+
if i + 1 < len(type_str) and type_str[i + 1] == ">":
|
|
47
|
+
tokens.append("->")
|
|
48
|
+
i += 2
|
|
49
|
+
continue
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError(f"Invalid character '-' at position {i}")
|
|
52
|
+
|
|
53
|
+
# Handle variable names (alphanumeric and underscores)
|
|
54
|
+
if char.isalnum() or char == "_":
|
|
55
|
+
var_name = ""
|
|
56
|
+
while i < len(type_str) and (type_str[i].isalnum() or type_str[i] == "_"):
|
|
57
|
+
var_name += type_str[i]
|
|
58
|
+
i += 1
|
|
59
|
+
tokens.append(var_name)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# Invalid character
|
|
63
|
+
raise ValueError(f"Invalid character '{char}' at position {i}")
|
|
64
|
+
|
|
65
|
+
return tokens
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_type(tokens: List[str]) -> tuple["BaseType", List[str]]:
|
|
69
|
+
"""
|
|
70
|
+
Parse a list of tokens into a type.
|
|
71
|
+
|
|
72
|
+
Uses recursive descent parsing with right-associativity for the arrow operator.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
tokens: List of tokens to parse
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
tuple[BaseType, List[str]]: Parsed type and remaining tokens
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: If tokens cannot be parsed
|
|
82
|
+
"""
|
|
83
|
+
from implica.core.types import Application
|
|
84
|
+
|
|
85
|
+
if not tokens:
|
|
86
|
+
raise ValueError("Unexpected end of input")
|
|
87
|
+
|
|
88
|
+
# Parse the left-hand side (either a variable or a parenthesized type)
|
|
89
|
+
left, remaining = parse_primary(tokens)
|
|
90
|
+
|
|
91
|
+
# Check if we have an arrow operator
|
|
92
|
+
if remaining and remaining[0] == "->":
|
|
93
|
+
# Consume the arrow
|
|
94
|
+
remaining = remaining[1:]
|
|
95
|
+
|
|
96
|
+
# Parse the right-hand side (right-associative)
|
|
97
|
+
right, remaining = parse_type(remaining)
|
|
98
|
+
|
|
99
|
+
# Create application
|
|
100
|
+
return Application(input_type=left, output_type=right), remaining
|
|
101
|
+
|
|
102
|
+
# No arrow, just return the primary type
|
|
103
|
+
return left, remaining
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_primary(tokens: List[str]) -> tuple["BaseType", List[str]]:
|
|
107
|
+
"""
|
|
108
|
+
Parse a primary type (variable or parenthesized type).
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
tokens: List of tokens to parse
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
tuple[BaseType, List[str]]: Parsed type and remaining tokens
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If tokens cannot be parsed
|
|
118
|
+
"""
|
|
119
|
+
from implica.core.types import Variable
|
|
120
|
+
|
|
121
|
+
if not tokens:
|
|
122
|
+
raise ValueError("Unexpected end of input")
|
|
123
|
+
|
|
124
|
+
token = tokens[0]
|
|
125
|
+
|
|
126
|
+
# Handle parenthesized type
|
|
127
|
+
if token == "(":
|
|
128
|
+
# Find matching closing parenthesis
|
|
129
|
+
depth = 1
|
|
130
|
+
i = 1
|
|
131
|
+
while i < len(tokens) and depth > 0:
|
|
132
|
+
if tokens[i] == "(":
|
|
133
|
+
depth += 1
|
|
134
|
+
elif tokens[i] == ")":
|
|
135
|
+
depth -= 1
|
|
136
|
+
i += 1
|
|
137
|
+
|
|
138
|
+
if depth != 0:
|
|
139
|
+
raise ValueError("Mismatched parentheses")
|
|
140
|
+
|
|
141
|
+
# Parse the content inside parentheses
|
|
142
|
+
inner_tokens = tokens[1 : i - 1]
|
|
143
|
+
if not inner_tokens:
|
|
144
|
+
raise ValueError("Empty parentheses")
|
|
145
|
+
|
|
146
|
+
result, inner_remaining = parse_type(inner_tokens)
|
|
147
|
+
|
|
148
|
+
if inner_remaining:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Unexpected tokens inside parentheses: {' '.join(inner_remaining)}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return result, tokens[i:]
|
|
154
|
+
|
|
155
|
+
# Handle variable name
|
|
156
|
+
if token not in ["->", ")"]:
|
|
157
|
+
return Variable(name=token), tokens[1:]
|
|
158
|
+
|
|
159
|
+
raise ValueError(f"Unexpected token: {token}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|