jjinx 0.0.1__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.
jinx/vocabulary.py ADDED
@@ -0,0 +1,181 @@
1
+ """J Vocabulary.
2
+
3
+ Building blocks / parts of speech for the J language.
4
+
5
+ The objects here are not tied to any implementation details needed for
6
+ execution (e.g. a verb is not tied to the code that will execute it).
7
+
8
+ The objects are just used to tag the words in the sentence so that they
9
+ can be evaluated at run time according to the context they are used in.
10
+
11
+ Resources:
12
+ - https://code.jsoftware.com/wiki/Vocabulary/Nouns
13
+ - https://code.jsoftware.com/wiki/Vocabulary/Words
14
+ - https://code.jsoftware.com/wiki/Vocabulary/Glossary
15
+
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass, field
21
+ from enum import Enum, auto
22
+ from typing import Callable, NamedTuple, Sequence
23
+
24
+ # Rank can be an integer or infinite (a float). It can't be any other float value
25
+ # but the type system does not make this easy to express.
26
+ RankT = int | float
27
+
28
+
29
+ class Word(NamedTuple):
30
+ """Sequence of characters that can be recognised as a part of the J language."""
31
+
32
+ value: str
33
+ """The string value of the word."""
34
+
35
+ is_numeric: bool
36
+ """Whether the word represents a numeric value (e.g. an integer or float)."""
37
+
38
+ start: int
39
+ """The start index of the word in the expression."""
40
+
41
+ end: int
42
+ """The end index of the word in the expression (exclusive, so `expression[start:end]` is the value)."""
43
+
44
+
45
+ class DataType(Enum):
46
+ Integer = auto()
47
+ Float = auto()
48
+ Byte = auto()
49
+ Box = auto()
50
+
51
+
52
+ @dataclass
53
+ class Noun[T]:
54
+ data_type: DataType
55
+ """Data type of value."""
56
+
57
+ data: Sequence[int | float | str] = field(default_factory=list)
58
+ """Data to represent the value itself, parsed from the word."""
59
+
60
+ implementation: T = None # type: ignore[assignment]
61
+ """Implementation of the noun, e.g. a NumPy array."""
62
+
63
+
64
+ @dataclass
65
+ class Monad[T]:
66
+ name: str
67
+ """Name of the monadic verb."""
68
+
69
+ rank: RankT
70
+ """Rank of monadic valence of the verb."""
71
+
72
+ function: Callable[[T], T] | Verb[T] = None # type: ignore[assignment]
73
+ """Function to execute the monadic verb, or another Verb object. Initially
74
+ set to None and then updated at runtime."""
75
+
76
+
77
+ @dataclass
78
+ class Dyad[T]:
79
+ name: str
80
+ """Name of the dyadic verb."""
81
+
82
+ left_rank: RankT
83
+ """Left rank of the dyadic verb."""
84
+
85
+ right_rank: RankT
86
+ """Right rank of the dyadic verb."""
87
+
88
+ function: Callable[[T, T], T] | Verb[T] = None # type: ignore[assignment]
89
+ """Function to execute the monadic verb, or another Verb object. Initially
90
+ set to None and then updated at runtime."""
91
+
92
+ is_commutative: bool = False
93
+ """Whether the dyadic verb is commutative."""
94
+
95
+
96
+ @dataclass
97
+ class Verb[T]:
98
+ spelling: str
99
+ """The symbolic spelling of the verb, e.g. `+`."""
100
+
101
+ name: str
102
+ """The name of the verb, e.g. `PLUS`, or its spelling if not a primitive J verb."""
103
+
104
+ monad: Monad[T] | None = None
105
+ """The monadic form of the verb, if it exists."""
106
+
107
+ dyad: Dyad[T] | None = None
108
+ """The dyadic form of the verb, if it exists."""
109
+
110
+ obverse: Verb[T] | str | None = None
111
+ """The obverse of the verb, if it exists. This is typically the inverse of the verb."""
112
+
113
+ def __str__(self):
114
+ return self.spelling
115
+
116
+ def __repr__(self):
117
+ return self.spelling
118
+
119
+
120
+ @dataclass
121
+ class Adverb[T]:
122
+ spelling: str
123
+ """The symbolic spelling of the adverb, e.g. `/`."""
124
+
125
+ name: str
126
+ """The name of the adverb, e.g. `SLASH`."""
127
+
128
+ monad: Monad[T] | None = None
129
+ """The monadic form of the adverb, if it exists."""
130
+
131
+ dyad: Dyad[T] | None = None
132
+ """The dyadic form of the adverb, if it exists."""
133
+
134
+ function: Callable[[Verb[T] | Noun[T]], Verb[T]] = None # type: ignore[assignment]
135
+ """Function of a single argument to implement the adverb."""
136
+
137
+
138
+ @dataclass
139
+ class Conjunction[T]:
140
+ spelling: str
141
+ """The symbolic spelling of the conjunction, e.g. `@:`."""
142
+
143
+ name: str
144
+ """The name of the conjunction, e.g. `ATCO`."""
145
+
146
+ function: Callable[[Verb[T] | Noun[T], Verb[T] | Noun[T]], Verb[T] | Noun[T]] = None # type: ignore[assignment]
147
+ """Function of a two arguments to implement the conjunction."""
148
+
149
+
150
+ @dataclass
151
+ class Copula:
152
+ spelling: str
153
+ """The symbolic spelling of the copula, e.g. `=.`."""
154
+
155
+ name: str
156
+ """The name of the copula, e.g. `EQCO`."""
157
+
158
+
159
+ @dataclass
160
+ class Punctuation:
161
+ spelling: str
162
+ """The symbolic spelling of the punctuation symbol, e.g. `(`."""
163
+
164
+ name: str
165
+ """The name of the punctuation, e.g. `LPAREN`."""
166
+
167
+
168
+ @dataclass
169
+ class Comment:
170
+ spelling: str
171
+ """The string value of the comment."""
172
+
173
+
174
+ @dataclass
175
+ class Name:
176
+ spelling: str
177
+ """The string value of the name."""
178
+
179
+
180
+ PunctuationT = Punctuation | Comment
181
+ PartOfSpeechT = Noun | Verb | Adverb | Conjunction | PunctuationT | Copula | Name
@@ -0,0 +1,375 @@
1
+ """Parsing and evaluation.
2
+
3
+ In J parsing and evaluation happen simultaneously. A fragment of the sentence is matched
4
+ against a set of 8 patterns. When a match is found the corresponding operation is executed
5
+ and the fragment is replaced with the result. This continues until no more matches are found.
6
+
7
+ In Jinx the execution may be done using different backends. An Executor instance with methods
8
+ for executing on each pattern is passed by the caller.
9
+
10
+ https://www.jsoftware.com/ioj/iojSent.htm#Parsing
11
+ https://www.jsoftware.com/help/jforc/parsing_and_execution_ii.htm
12
+ https://code.jsoftware.com/wiki/Vocabulary/Modifiers
13
+
14
+ """
15
+
16
+ from typing import cast
17
+
18
+ from jinx.errors import EvaluationError, JinxNotImplementedError, JSyntaxError
19
+ from jinx.execution.executor import Executor
20
+ from jinx.primitives import PRIMITIVES
21
+ from jinx.vocabulary import (
22
+ Adverb,
23
+ Comment,
24
+ Conjunction,
25
+ Copula,
26
+ Name,
27
+ Noun,
28
+ PartOfSpeechT,
29
+ Punctuation,
30
+ Verb,
31
+ )
32
+ from jinx.word_formation import form_words
33
+ from jinx.word_spelling import spell_words
34
+
35
+
36
+ def str_(executor: Executor, word: PartOfSpeechT | str) -> str:
37
+ if isinstance(word, str):
38
+ return word
39
+ if isinstance(word, Noun):
40
+ return executor.noun_to_string(word)
41
+ elif isinstance(word, Verb | Adverb | Conjunction):
42
+ return word.spelling
43
+ elif isinstance(word, Name):
44
+ return word.spelling
45
+ elif isinstance(word, Punctuation | Copula):
46
+ return word.spelling
47
+ else:
48
+ raise NotImplementedError(f"Cannot print word of type {type(word)}")
49
+
50
+
51
+ def print_words(
52
+ executor: Executor, word: PartOfSpeechT, variables: dict[str, PartOfSpeechT]
53
+ ) -> None:
54
+ value = (
55
+ str_(executor, variables[word.spelling])
56
+ if isinstance(word, Name)
57
+ else str_(executor, word)
58
+ )
59
+ print(value)
60
+
61
+
62
+ def evaluate_single_verb_sentence(
63
+ executor: Executor, sentence: str, variables: dict[str, PartOfSpeechT]
64
+ ) -> Verb:
65
+ tokens = form_words(sentence)
66
+ words = spell_words(tokens)
67
+ result = _evaluate_words(executor, words, variables)
68
+ if not isinstance(result, Verb):
69
+ raise EvaluationError(f"Expected a verb, got {type(result).__name__}")
70
+ return result
71
+
72
+
73
+ def build_verb_noun_phrase(
74
+ executor: Executor,
75
+ words: list[Verb | Noun | Adverb | Conjunction],
76
+ ) -> Verb | Noun | None:
77
+ """Build the verb or noun phrase from a list of words, or raise an error."""
78
+ while len(words) > 1:
79
+ match words:
80
+ case [left, Adverb(), *remaining]:
81
+ result = executor.apply_adverb(left, words[1]) # type: ignore[arg-type]
82
+ words = [result, *remaining]
83
+
84
+ case [left, Conjunction(), right, *remaining]:
85
+ result = executor.apply_conjunction(left, words[1], right) # type: ignore[arg-type]
86
+ words = [result, *remaining]
87
+
88
+ case _:
89
+ raise EvaluationError("Unable to build verb/noun phrase")
90
+
91
+ if not words:
92
+ return None
93
+
94
+ if isinstance(words[0], Verb | Noun):
95
+ return words[0]
96
+
97
+ raise EvaluationError("Unable to build verb/noun phrase")
98
+
99
+
100
+ def evaluate_words(
101
+ executor: Executor,
102
+ words: list[PartOfSpeechT],
103
+ variables: dict[str, PartOfSpeechT] | None = None,
104
+ level: int = 0,
105
+ ) -> PartOfSpeechT:
106
+ if variables is None:
107
+ variables = {}
108
+
109
+ # Ensure noun and verb implementations are set according to the chosen execution
110
+ # framework (this is just NumPy for now).
111
+ for word in words:
112
+ if isinstance(word, Noun):
113
+ executor.ensure_noun_implementation(word)
114
+
115
+ all_primitives = (
116
+ executor.primitive_verb_map
117
+ | executor.primitive_adverb_map
118
+ | executor.primitive_conjuction_map
119
+ )
120
+
121
+ for primitive in PRIMITIVES:
122
+ if primitive.name not in all_primitives:
123
+ continue
124
+ if isinstance(primitive, Verb):
125
+ monad, dyad = executor.primitive_verb_map[primitive.name]
126
+ if primitive.monad is not None and monad is not None:
127
+ primitive.monad.function = monad
128
+ if primitive.dyad is not None and dyad is not None:
129
+ primitive.dyad.function = dyad
130
+ if isinstance(primitive, Adverb):
131
+ primitive.function = executor.primitive_adverb_map[primitive.name] # type: ignore[assignment]
132
+ if isinstance(primitive, Conjunction):
133
+ primitive.function = executor.primitive_conjuction_map[primitive.name]
134
+
135
+ # Verb obverses are converted from strings to Verb objects.
136
+ for word in words:
137
+ if isinstance(word, Verb) and isinstance(word.obverse, str):
138
+ verb = evaluate_single_verb_sentence(executor, word.obverse, variables)
139
+ word.obverse = verb
140
+
141
+ return _evaluate_words(executor, words, variables, level=level)
142
+
143
+
144
+ def get_parts_to_left(
145
+ executor: Executor,
146
+ word: PartOfSpeechT,
147
+ words: list[PartOfSpeechT],
148
+ current_level: int,
149
+ variables: dict[str, PartOfSpeechT],
150
+ ) -> list[Noun | Verb | Adverb | Conjunction]:
151
+ """Get the parts of speach to the left of the current word, modifying list of remaining words.
152
+
153
+ This method is called when the last word we encountered is an adverb or conjunction and
154
+ a verb or noun phrase is expected to the left of it.
155
+ """
156
+ parts_to_left: list[Noun | Verb | Adverb | Conjunction] = []
157
+
158
+ while words:
159
+ word = resolve_word(word, variables)
160
+ # A verb/noun phrase starts with a verb/noun which does not have a conjunction to its left.
161
+ if isinstance(word, Noun | Verb):
162
+ if not isinstance(words[-1], Conjunction):
163
+ parts_to_left = [word, *parts_to_left]
164
+ break
165
+ else:
166
+ conjunction = cast(Conjunction, words.pop())
167
+ parts_to_left = [conjunction, word, *parts_to_left]
168
+
169
+ elif isinstance(word, Adverb | Conjunction):
170
+ parts_to_left = [word, *parts_to_left]
171
+
172
+ elif isinstance(word, Punctuation) and word.spelling == ")":
173
+ word = evaluate_words(executor, words, level=current_level + 1)
174
+ continue
175
+
176
+ else:
177
+ break
178
+
179
+ if words:
180
+ word = words.pop()
181
+
182
+ return parts_to_left
183
+
184
+
185
+ def resolve_word(
186
+ word: PartOfSpeechT, variables: dict[str, PartOfSpeechT]
187
+ ) -> PartOfSpeechT:
188
+ """Find the Verb/Adverb/Conjunction/Noun that a name is assigned to.
189
+
190
+ If we encounter a cycle of names, return the original name.
191
+ """
192
+ if not isinstance(word, Name):
193
+ return word
194
+
195
+ original_name = word
196
+ visited = set()
197
+ while True:
198
+ visited.add(word.spelling)
199
+ if word.spelling not in variables:
200
+ return word
201
+ assignment = variables[word.spelling]
202
+ if not isinstance(assignment, Name):
203
+ return assignment
204
+ word = assignment
205
+ if word.spelling in visited:
206
+ return original_name
207
+
208
+
209
+ def _evaluate_words(
210
+ executor: Executor, words: list[PartOfSpeechT], variables, level: int = 0
211
+ ) -> PartOfSpeechT:
212
+ # If the first word is None, prepend a None to the list denoting the left-most
213
+ # edge of the expression.
214
+ if words[0] is not None:
215
+ words = [None, *words] # type: ignore[list-item]
216
+
217
+ fragment: list[PartOfSpeechT] = []
218
+ result: PartOfSpeechT
219
+
220
+ while words:
221
+ word = words.pop()
222
+
223
+ if isinstance(word, Comment):
224
+ continue
225
+
226
+ elif isinstance(word, (Punctuation, Copula)):
227
+ word = word.spelling # type: ignore[assignment]
228
+
229
+ # If the next word closes a parenthesis, we need to evaluate the words inside it
230
+ # first to get the next word to prepend to the fragment.
231
+ if word == ")":
232
+ word = evaluate_words(executor, words, variables, level=level + 1)
233
+
234
+ # If the fragment has a modifier (adverb/conjunction) at the start, we need to find the
235
+ # entire verb/noun phrase to the left as the next word to prepend to the fragment.
236
+ # Contrary to usual parsing and evaluation, the verb/noun phrase is evaluated left-to-right.
237
+ if fragment and isinstance(
238
+ resolve_word(fragment[0], variables), Adverb | Conjunction
239
+ ):
240
+ parts_to_left = get_parts_to_left(
241
+ executor, word, words, level, variables=variables
242
+ )
243
+ # parts_to_left may be empty if the conjunction is the target of an assignment
244
+ # or enclosed in parentheses.
245
+ if parts_to_left:
246
+ word = build_verb_noun_phrase(executor, parts_to_left) # type: ignore[assignment]
247
+
248
+ fragment = [word, *fragment]
249
+
250
+ # fmt: off
251
+ while True:
252
+
253
+ # 7. Is
254
+ # This case (assignment) is checked separately, before names are substituted with their values.
255
+ # For now we treat =. and =: the same.
256
+ match fragment:
257
+ case Name(), "=." | "=:", Conjunction() | Adverb() | Verb() | Noun() | Name(), *_:
258
+ name, _, cavn, *last = fragment
259
+ name = cast(Name, name)
260
+ variables[name.spelling] = cavn
261
+ fragment = [name, *last]
262
+ continue
263
+
264
+ # Substitute variable names with their values and do pattern matching. If a match occurs
265
+ # the original fragment (list of unsubstituted names) is modified.
266
+ fragment_ = [resolve_word(word, variables) for word in fragment]
267
+
268
+ match fragment_:
269
+
270
+ # 0. Monad
271
+ case None | "=." | "=:" | "(", Verb(), Noun():
272
+ edge, verb, noun = fragment_
273
+ result = executor.apply_monad(verb, noun) # type: ignore[arg-type]
274
+ if edge == "(" and level > 0:
275
+ return result
276
+ fragment[1:] = [result]
277
+
278
+ # 1. Monad
279
+ case None | "=." | "=:" | "(" | Adverb() | Verb() | Noun(), Adverb() | Verb(), Verb(), Noun():
280
+ edge, _, verb, noun = fragment_
281
+ result = executor.apply_monad(verb, noun) # type: ignore[arg-type]
282
+ fragment[2:] = [result]
283
+
284
+ # 2. Dyad
285
+ case None | "=." | "=:" | "(" | Adverb() | Verb() | Noun(), Noun(), Verb(), Noun():
286
+ edge, noun, verb, noun_2 = fragment_
287
+ result = executor.apply_dyad(verb, noun, noun_2) # type: ignore[arg-type]
288
+ if edge == "(" and level > 0:
289
+ return result
290
+ fragment[1:] = [result]
291
+
292
+ # 3. Adverb
293
+ case (
294
+ None | "=." | "=:" | "(" | Adverb() | Verb() | Noun(),
295
+ Verb() | Noun(),
296
+ Adverb(),
297
+ *_,
298
+ ):
299
+ edge, verb, adverb, *last = fragment_
300
+ result = executor.apply_adverb(verb, adverb) # type: ignore[arg-type]
301
+ if edge == "(" and last == [")"] and level > 0:
302
+ return result
303
+ fragment[1:3] = [result]
304
+
305
+ # 4. Conjunction
306
+ case (
307
+ None | "=." | "=:" | "(" | Adverb() | Verb() | Noun(),
308
+ Verb() | Noun(),
309
+ Conjunction(),
310
+ Verb() | Noun(),
311
+ *_,
312
+ ):
313
+ edge, verb_or_noun_1, conjunction, verb_or_noun_2, *last = fragment_
314
+ result = executor.apply_conjunction(verb_or_noun_1, conjunction, verb_or_noun_2) # type: ignore[arg-type]
315
+ if edge == "(" and last == [")"] and level > 0:
316
+ return result
317
+ fragment[1:4] = [result]
318
+
319
+ # 5. Fork
320
+ case (
321
+ None | "=." | "=:" | "(" | Adverb() | Verb() | Noun(),
322
+ Verb() | Noun(),
323
+ Verb(),
324
+ Verb(),
325
+ ):
326
+ edge, verb_or_noun_1, verb_2, verb_3 = fragment_
327
+ result = executor.build_fork(verb_or_noun_1, verb_2, verb_3) # type: ignore[arg-type]
328
+ if edge == "(" and level > 0:
329
+ return result
330
+ fragment[1:4] = [result]
331
+
332
+ # 6. Hook/Adverb
333
+ case (
334
+ None | "=." | "=:" | "(",
335
+ Conjunction() | Adverb() | Verb() | Noun(),
336
+ Conjunction() | Adverb() | Verb() | Noun(),
337
+ *_,
338
+ ):
339
+ edge, cavn1, cavn2, *last = fragment_
340
+ match [cavn1, cavn2]:
341
+ case [Verb(), Verb()]:
342
+ result = executor.build_hook(cavn1, cavn2) # type: ignore[arg-type]
343
+ case [Adverb(), Adverb()] | [Conjunction() , Noun()] | [Conjunction(), Verb()] | [Noun(), Conjunction()] | [Verb(), Conjunction()]:
344
+ # These are valid combinations but not implemented yet.
345
+ raise JinxNotImplementedError(
346
+ f"Jinx error: currently only 'Verb Verb' is implemented for hook/adverb matching, got "
347
+ f"({type(cavn1).__name__} {type(cavn2).__name__})"
348
+ )
349
+ case _:
350
+ raise JSyntaxError(f"syntax error: unexecutable fragment ({type(cavn1).__name__} {type(cavn2).__name__})")
351
+ if edge == "(" and level > 0:
352
+ return result
353
+ fragment[1:] = [result]
354
+
355
+ # 8. Parentheses
356
+ # Differs from the J source as it does not match ")" and instead checks
357
+ # the level to ensure that "(" is balanced.
358
+ case ["(", Conjunction() | Adverb() | Verb() | Noun()]:
359
+ _, cavn = fragment_
360
+ if level > 0:
361
+ return cast(PartOfSpeechT, cavn)
362
+ raise EvaluationError("Unbalanced parentheses")
363
+
364
+ # Non-executable fragment.
365
+ case _:
366
+ break
367
+
368
+ # fmt: on
369
+
370
+ if len(fragment) > 2:
371
+ raise EvaluationError(
372
+ f"Unexecutable fragment: {[str_(executor, w) for w in fragment if w is not None]}"
373
+ )
374
+
375
+ return fragment[1]