implica 0.3.3__py3-none-any.whl → 0.4.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -29,6 +29,7 @@ __all__ = [
29
29
  "Application",
30
30
  "var",
31
31
  "app",
32
+ "type_from_string",
32
33
  "Combinator",
33
34
  "S",
34
35
  "K",
@@ -41,4 +42,4 @@ __all__ = [
41
42
  "mutations",
42
43
  ]
43
44
 
44
- __version__ = "0.3.3"
45
+ __version__ = "0.4.0"
implica/core/__init__.py CHANGED
@@ -1,4 +1,14 @@
1
- from .types import BaseType, Variable, Application, var, app
1
+ from .types import BaseType, Variable, Application, var, app, type_from_string
2
2
  from .combinator import Combinator, S, K
3
3
 
4
- __all__ = ["BaseType", "Variable", "Application", "var", "app", "Combinator", "S", "K"]
4
+ __all__ = [
5
+ "BaseType",
6
+ "Variable",
7
+ "Application",
8
+ "var",
9
+ "app",
10
+ "type_from_string",
11
+ "Combinator",
12
+ "S",
13
+ "K",
14
+ ]
implica/core/types.py CHANGED
@@ -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,7 @@
1
+ """
2
+ Utility functions for the implica package.
3
+ """
4
+
5
+ from .parsing import tokenize, parse_type, parse_primary
6
+
7
+ __all__ = ["tokenize", "parse_type", "parse_primary"]
@@ -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}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: implica
3
- Version: 0.3.3
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
@@ -1,7 +1,7 @@
1
- implica/__init__.py,sha256=0mZ2GjzddOh36dHFon3fW8RIykgyO5KNwYQ5_9RR-Yc,886
2
- implica/core/__init__.py,sha256=opdPtga0OU0d8CEf5DL4Xl4AriBaOHdVW4mbwdD2A0Y,191
1
+ implica/__init__.py,sha256=BD-fo6jkUvyETCLqulTeVUHrha0jPyxTPgLIMlvV-zo,910
2
+ implica/core/__init__.py,sha256=gwUoJDHZueECsh_X4KXp_6NX1A_tMNu-wJKXjwstzlU,268
3
3
  implica/core/combinator.py,sha256=VOeIj_FRMI7fmuMjjbgRBP-FvExnQ6vOjCD-egt96lI,4324
4
- implica/core/types.py,sha256=AtzQAY9rJ4lIcjqdVZHLDOO-YXSudez4NOzaw6EPjRc,5230
4
+ implica/core/types.py,sha256=QQZ61JfG9CxaiTzMXfg9jA9NfxuZbG8MnEzhy38w4Pw,6647
5
5
  implica/graph/__init__.py,sha256=Lj2bUdPZVgMoIYXF7DeMNuFBl3ftqrkAxhGq-Oj0MoQ,172
6
6
  implica/graph/connection.py,sha256=IpyvA_J_BQqhecxN3XoQ00Io7zjwok5PGz2TKia35ZQ,13746
7
7
  implica/graph/elements.py,sha256=ag5Gdr-5djGnKOEhoEAm3NpWTmw3gnucq1IEv2xxTB4,6015
@@ -24,7 +24,9 @@ implica/mutations/try_remove_edge.py,sha256=2aeg6f6nR49MViyZk15-ys7I5Xt0a2wXycBI
24
24
  implica/mutations/try_remove_many_edges.py,sha256=pgZdqFltMaycNBfdjejYVXQZw3i2K1AJYYcsaKk7n74,1596
25
25
  implica/mutations/try_remove_many_nodes.py,sha256=DK_BtKuEh_7zm5bzryOi1BVfmuCiQ-SVA9yIdgN-imE,1817
26
26
  implica/mutations/try_remove_node.py,sha256=igTEnGUCd0U2_3Rr5SIOSIs4FZmYi2Y6bLk0Ae64Bsg,1837
27
- implica-0.3.3.dist-info/LICENSE,sha256=jDn9mmsCvjx_av9marP7DBqjfmK1gsQmgycgBRC2Eck,1073
28
- implica-0.3.3.dist-info/METADATA,sha256=d6yVeAJq0yVrKVdMcBxgEu8-SUkcpH0f2GnePDPE4sU,25200
29
- implica-0.3.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
30
- implica-0.3.3.dist-info/RECORD,,
27
+ implica/utils/__init__.py,sha256=zh2OscD5SQ3-YLgsTm3-loTExSuhRRbdZTFFVy3Fxdk,164
28
+ implica/utils/parsing.py,sha256=z7ofQ_tmPaGf3HTOwT1EuANJyIjMpB245mX6URBRHnw,4158
29
+ implica-0.4.0.dist-info/LICENSE,sha256=jDn9mmsCvjx_av9marP7DBqjfmK1gsQmgycgBRC2Eck,1073
30
+ implica-0.4.0.dist-info/METADATA,sha256=ZdKzwDnAJxpbSN_O9r0VMsgHnO6owsqswzghs7alS-o,38241
31
+ implica-0.4.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
+ implica-0.4.0.dist-info/RECORD,,