liminate 0.1.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.
liminate/__init__.py ADDED
@@ -0,0 +1,49 @@
1
+ """Liminate Programming Language v1 / v2a / v2b / v2c / v2d / v3a."""
2
+
3
+ from .adapter import (
4
+ Adapter,
5
+ AdapterDone,
6
+ AdapterFailure,
7
+ AdapterUpdate,
8
+ DomainPack,
9
+ LiveValueDeclaration,
10
+ LiveValueEntry,
11
+ LiveValueRegistry,
12
+ TestAdapter,
13
+ TestDomainPack,
14
+ )
15
+ from .analyzer import SymbolEntry, analyze
16
+ from .cli import Session
17
+ from .interpreter import execute
18
+ from .listener import listen
19
+ from .lexer import leading_indent, tokenize
20
+ from .parser import parse, parse_when_block
21
+ from .renderer import render
22
+ from .reorderer import reorder
23
+ from .result import LiminateResult, ResultStatus
24
+
25
+ __all__ = [
26
+ "Adapter",
27
+ "AdapterDone",
28
+ "AdapterFailure",
29
+ "AdapterUpdate",
30
+ "DomainPack",
31
+ "LiminateResult",
32
+ "LiveValueDeclaration",
33
+ "LiveValueEntry",
34
+ "LiveValueRegistry",
35
+ "ResultStatus",
36
+ "Session",
37
+ "SymbolEntry",
38
+ "TestAdapter",
39
+ "TestDomainPack",
40
+ "analyze",
41
+ "execute",
42
+ "leading_indent",
43
+ "listen",
44
+ "parse",
45
+ "parse_when_block",
46
+ "render",
47
+ "reorder",
48
+ "tokenize",
49
+ ]
liminate/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for `python -m liminate`."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
liminate/adapter.py ADDED
@@ -0,0 +1,449 @@
1
+ """Adapter infrastructure for Liminate v3a event-driven execution.
2
+
3
+ Sources:
4
+ - v3a §116 (event sources: adapter contract — declaration, adapter, lifecycle)
5
+ - v3a §117 (live value lifecycle: declared → unset → active → inactive)
6
+ - v3a §118 (domain pack registration via constructor/CLI, not language syntax)
7
+ - v3a §119 (single-threaded event queue: adapters enqueue; interpreter
8
+ processes one update to completion before next dequeue)
9
+ - v3a §120 (adapter failure isolation: single crash doesn't kill the
10
+ interpreter; dependent handlers disable; queue keeps draining)
11
+
12
+ This module defines the contract by which external event sources (domain
13
+ packs) feed the Phase 2 reactive interpreter. v3a ships exactly one
14
+ adapter implementation: `TestAdapter`, used by integration tests and the
15
+ dogfood program. Real-world domain packs (healthcare, smart home, game)
16
+ are explicitly out of scope (§126).
17
+
18
+ The interpreter owns the event queue and the live-value registry; each
19
+ adapter contributes updates and declarations. Adapters never read from
20
+ the queue themselves — they only push.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from abc import ABC, abstractmethod
26
+ from dataclasses import dataclass, field
27
+ from queue import Queue
28
+ from typing import Any
29
+
30
+ from .vocabulary import (
31
+ PackVerbExecution,
32
+ PackVerbSignature,
33
+ PackVerbSlot,
34
+ )
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Declarations and queue messages (§116)
39
+ # ---------------------------------------------------------------------------
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class LiveValueDeclaration:
44
+ """A live value declared by a domain pack (§116).
45
+
46
+ `value_type` matches the analyzer's type strings: "number", "string",
47
+ "record", "list_of_numbers", "list_of_strings", "list_of_records".
48
+ For broad declarations the adapter's first update establishes the
49
+ final shape (§116).
50
+ """
51
+ name: str
52
+ value_type: str
53
+
54
+
55
+ @dataclass
56
+ class AdapterUpdate:
57
+ """A `(name, new_value)` pair pushed by an adapter (§119).
58
+
59
+ The interpreter consumes one update at a time, runs change detection
60
+ (§113 — deep value equality), and fires eligible handlers before
61
+ dequeuing the next."""
62
+ name: str
63
+ value: Any
64
+
65
+
66
+ @dataclass
67
+ class AdapterDone:
68
+ """Marker that an adapter has finished normally (§118 — "[done]"
69
+ in test sentence notation). The interpreter counts these against
70
+ the active-adapter set to decide when to shut down."""
71
+ adapter_name: str
72
+
73
+
74
+ @dataclass
75
+ class AdapterFailure:
76
+ """An adapter failure isolated to that adapter (§120). The
77
+ interpreter marks the adapter's live values inactive and disables
78
+ handlers that depend solely on them; other adapters keep running."""
79
+ adapter_name: str
80
+ reason: str
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Adapter and DomainPack ABCs (§116/§118)
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ class Adapter(ABC):
89
+ """Adapter contract (§116).
90
+
91
+ An adapter pushes `(name, value)` updates into a shared, thread-safe
92
+ queue owned by the interpreter. Concrete adapters override `start`
93
+ (begin pushing) and `stop` (cease pushing). `attach_queue` is called
94
+ by the interpreter before `start` to wire up the queue.
95
+
96
+ `name` is a human-readable identifier surfaced in error and
97
+ shutdown metadata (§120 / §122).
98
+
99
+ **Termination contract.** For the listener's Phase 2 drain loop to
100
+ exit normally, every adapter must eventually push exactly one
101
+ `AdapterDone(adapter_name=self.name)` (natural completion) or one
102
+ `AdapterFailure(...)` (error). Adapters that run forever and never
103
+ signal completion will keep the listener alive until the user's
104
+ program calls `finish` (which triggers stop() from inside the
105
+ listener) or the process is interrupted externally.
106
+
107
+ `stop()` is invoked by the listener — either from `_shutdown_finish`
108
+ after a `finish` propagates, or from the final cleanup loop after
109
+ the drain has already returned. Concrete adapters should treat
110
+ `stop()` as "cease pushing as soon as practical and tear down any
111
+ threads or resources." They do not need to push a terminal
112
+ `AdapterDone` from within `stop()` itself — by that point the
113
+ listener has already decided to shut down.
114
+ """
115
+
116
+ def __init__(self, name: str = "adapter"):
117
+ self.name = name
118
+ self.queue: Queue | None = None
119
+ self.started = False
120
+ self.stopped = False
121
+
122
+ def attach_queue(self, queue: Queue) -> None:
123
+ """Called by the interpreter before `start`. The adapter
124
+ uses `self.queue.put(...)` to enqueue updates from this point."""
125
+ self.queue = queue
126
+
127
+ @abstractmethod
128
+ def start(self) -> None:
129
+ """Begin pushing updates into the attached queue. Must not be
130
+ called before `attach_queue`."""
131
+ raise NotImplementedError
132
+
133
+ @abstractmethod
134
+ def stop(self) -> None:
135
+ """Cease pushing. Called by the interpreter on `finish` (§112),
136
+ on global completion (§120), or on external termination."""
137
+ raise NotImplementedError
138
+
139
+
140
+ class DomainPack(ABC):
141
+ """Domain pack contract (§118).
142
+
143
+ A pack groups one or more live value declarations with the adapter
144
+ that produces them. Packs are registered with the interpreter at
145
+ construction time — no Liminate-level `use`/`load` verb in v3a
146
+ (§118).
147
+ """
148
+
149
+ @abstractmethod
150
+ def name(self) -> str:
151
+ """Human-readable identifier — used in error messages and
152
+ shutdown metadata."""
153
+ raise NotImplementedError
154
+
155
+ @abstractmethod
156
+ def declarations(self) -> list[LiveValueDeclaration]:
157
+ """Return the live values this pack provides. The interpreter
158
+ registers each name in the symbol table before Phase 1 begins
159
+ (§116/§117)."""
160
+ raise NotImplementedError
161
+
162
+ @abstractmethod
163
+ def adapter(self) -> Adapter:
164
+ """Return the adapter that will push updates for this pack's
165
+ declared live values."""
166
+ raise NotImplementedError
167
+
168
+ # v4a §137 — optional pack contributions to the active vocabulary.
169
+ # Default to empty so existing packs (which override only the three
170
+ # required methods above) keep working unchanged. Backward-compatible.
171
+
172
+ def vocabulary(self) -> list[tuple[str, str]]:
173
+ """Pack-contributed vocabulary as `(word, category)` pairs. v4a
174
+ only consumes the noun category; other categories are reserved
175
+ for future extension."""
176
+ return []
177
+
178
+ def verbs(self) -> list[PackVerbSignature]:
179
+ """Pack-contributed verbs and their slot signatures (v4a §137).
180
+ Empty by default."""
181
+ return []
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # v4a §137 — JSON pack verb deserialization helper
186
+ # ---------------------------------------------------------------------------
187
+
188
+
189
+ def parse_pack_verb_signature(definition: dict) -> PackVerbSignature:
190
+ """Convert a single JSON `verbs[]` entry into a PackVerbSignature.
191
+
192
+ The JSON schema (§137):
193
+ {
194
+ "word": "<verb-name>",
195
+ "slots": [
196
+ {"name": "...", "connective": "...", "required": true,
197
+ "type_constraint": "..."}
198
+ ],
199
+ "execution": {"type": "set_value", "target_name": "...",
200
+ "source_slot": "..."}
201
+ }
202
+ """
203
+ word = definition["word"]
204
+ raw_slots = definition.get("slots", [])
205
+ slots: list[PackVerbSlot] = []
206
+ for s in raw_slots:
207
+ slots.append(
208
+ PackVerbSlot(
209
+ name=s["name"],
210
+ connective=s["connective"],
211
+ required=bool(s.get("required", True)),
212
+ type_constraint=s.get("type_constraint"),
213
+ )
214
+ )
215
+ exec_def = definition.get("execution") or {}
216
+ execution = PackVerbExecution(
217
+ type=exec_def.get("type", ""),
218
+ target_name=exec_def.get("target_name"),
219
+ source_slot=exec_def.get("source_slot"),
220
+ )
221
+ return PackVerbSignature(
222
+ word=word, slots=tuple(slots), execution=execution,
223
+ )
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # TestAdapter / TestDomainPack — the v3a-shipped test surface (§118)
228
+ # ---------------------------------------------------------------------------
229
+
230
+
231
+ class TestAdapter(Adapter):
232
+ """Adapter that pushes a scripted, finite sequence of updates.
233
+
234
+ The script is a list of items, each one of:
235
+ - `("name", value)` — an `AdapterUpdate` push
236
+ - `"[done]"` — an explicit `AdapterDone` push
237
+
238
+ `start()` drains the entire script onto the queue synchronously
239
+ (single-threaded test semantics — the interpreter consumes them in
240
+ order). If the script does not end with `"[done]"`, an
241
+ `AdapterDone` is appended automatically so the interpreter sees
242
+ normal completion (§118).
243
+
244
+ `stop()` is idempotent — repeated calls are safe.
245
+ """
246
+
247
+ # pytest collects classes whose names begin with "Test" by default;
248
+ # opt this Adapter implementation out so it isn't mistaken for a
249
+ # test suite.
250
+ __test__ = False
251
+
252
+ def __init__(
253
+ self,
254
+ script: list[tuple[str, Any] | str],
255
+ *,
256
+ name: str = "test-adapter",
257
+ ):
258
+ super().__init__(name=name)
259
+ self._script: list[tuple[str, Any] | str] = list(script)
260
+
261
+ def start(self) -> None:
262
+ if self.queue is None:
263
+ raise RuntimeError(
264
+ "TestAdapter.start() called before attach_queue()."
265
+ )
266
+ if self.started:
267
+ return
268
+ self.started = True
269
+ ended_with_done = False
270
+ for entry in self._script:
271
+ if isinstance(entry, str) and entry == "[done]":
272
+ self.queue.put(AdapterDone(adapter_name=self.name))
273
+ ended_with_done = True
274
+ continue
275
+ if isinstance(entry, tuple) and len(entry) == 2:
276
+ self.queue.put(
277
+ AdapterUpdate(name=entry[0], value=entry[1])
278
+ )
279
+ ended_with_done = False
280
+ continue
281
+ # Malformed script entry — surface as adapter failure so the
282
+ # interpreter can isolate it (§120).
283
+ self.queue.put(
284
+ AdapterFailure(
285
+ adapter_name=self.name,
286
+ reason=f"malformed script entry: {entry!r}",
287
+ )
288
+ )
289
+ return
290
+ if not ended_with_done:
291
+ # §118: every adapter must eventually signal normal
292
+ # completion (or fail) for the interpreter to decide when
293
+ # to shut down.
294
+ self.queue.put(AdapterDone(adapter_name=self.name))
295
+
296
+ def stop(self) -> None:
297
+ self.stopped = True
298
+
299
+
300
+ class TestDomainPack(DomainPack):
301
+ """DomainPack wrapping a TestAdapter and a fixed declaration list.
302
+
303
+ Used by Phase 11 integration tests and the v3a dogfood program to
304
+ drive event-driven sentences deterministically."""
305
+
306
+ # pytest collects classes whose names begin with "Test" by default;
307
+ # opt this DomainPack implementation out so it isn't mistaken for
308
+ # a test suite.
309
+ __test__ = False
310
+
311
+ def __init__(
312
+ self,
313
+ declarations: list[tuple[str, str]] | list[LiveValueDeclaration],
314
+ script: list[tuple[str, Any] | str],
315
+ *,
316
+ name: str = "test-pack",
317
+ vocabulary: list[tuple[str, str]] | None = None,
318
+ verbs: list[PackVerbSignature] | None = None,
319
+ ):
320
+ self._name = name
321
+ self._declarations: list[LiveValueDeclaration] = [
322
+ d if isinstance(d, LiveValueDeclaration)
323
+ else LiveValueDeclaration(name=d[0], value_type=d[1])
324
+ for d in declarations
325
+ ]
326
+ self._script = script
327
+ self._adapter: TestAdapter | None = None
328
+ self._vocabulary: list[tuple[str, str]] = list(vocabulary or [])
329
+ self._verbs: list[PackVerbSignature] = list(verbs or [])
330
+
331
+ def name(self) -> str:
332
+ return self._name
333
+
334
+ def declarations(self) -> list[LiveValueDeclaration]:
335
+ return list(self._declarations)
336
+
337
+ def adapter(self) -> Adapter:
338
+ if self._adapter is None:
339
+ self._adapter = TestAdapter(self._script, name=self._name)
340
+ return self._adapter
341
+
342
+ def vocabulary(self) -> list[tuple[str, str]]:
343
+ return list(self._vocabulary)
344
+
345
+ def verbs(self) -> list[PackVerbSignature]:
346
+ return list(self._verbs)
347
+
348
+
349
+ # ---------------------------------------------------------------------------
350
+ # Live-value registry (§117)
351
+ # ---------------------------------------------------------------------------
352
+
353
+
354
+ @dataclass
355
+ class LiveValueEntry:
356
+ """Per-live-value bookkeeping the interpreter consults during
357
+ Phase 2 (§117).
358
+
359
+ `status` transitions:
360
+ unset -> initial state; conditions involving this name
361
+ evaluate as false (§113).
362
+ active -> at least one value has been received (Phase 1 init
363
+ or first adapter update); conditions evaluate
364
+ normally.
365
+ inactive -> the owning adapter failed (§117/§120); dependent
366
+ handlers are disabled.
367
+ """
368
+ name: str
369
+ value_type: str
370
+ adapter_name: str
371
+ status: str = "unset"
372
+
373
+
374
+ class LiveValueRegistry:
375
+ """Tracks which symbol-table names are adapter-owned and what
376
+ state their adapter is in (§117/§120).
377
+
378
+ Used by:
379
+ - The analyzer (via `live_value_names`) to enforce ownership rules
380
+ (§111: `remember`/`filter` restrictions).
381
+ - The interpreter to update status on adapter updates and failures,
382
+ and to disable handlers when adapters die.
383
+ """
384
+
385
+ def __init__(self) -> None:
386
+ self._entries: dict[str, LiveValueEntry] = {}
387
+
388
+ def declare(
389
+ self, decl: LiveValueDeclaration, adapter_name: str,
390
+ ) -> None:
391
+ """Register a live value before Phase 1 (§117 step 1)."""
392
+ if decl.name in self._entries:
393
+ raise ValueError(
394
+ f"live value '{decl.name}' was already declared by "
395
+ f"'{self._entries[decl.name].adapter_name}' — v3a §116 "
396
+ f"disallows multiple adapters providing the same name."
397
+ )
398
+ self._entries[decl.name] = LiveValueEntry(
399
+ name=decl.name,
400
+ value_type=decl.value_type,
401
+ adapter_name=adapter_name,
402
+ )
403
+
404
+ def names(self) -> set[str]:
405
+ """All declared live-value names (for the analyzer)."""
406
+ return set(self._entries.keys())
407
+
408
+ def active_names(self) -> set[str]:
409
+ """Live-value names whose owning adapter is still active —
410
+ excludes any whose adapter has failed (§120). The interpreter
411
+ uses this to decide which handlers remain eligible."""
412
+ return {
413
+ n for n, e in self._entries.items() if e.status != "inactive"
414
+ }
415
+
416
+ def entry(self, name: str) -> LiveValueEntry | None:
417
+ return self._entries.get(name)
418
+
419
+ def mark_active(self, name: str) -> None:
420
+ """Called by the interpreter after the first valid value is
421
+ seen for `name` (§117 — `unset` → `active`)."""
422
+ entry = self._entries.get(name)
423
+ if entry is None:
424
+ return
425
+ if entry.status == "unset":
426
+ entry.status = "active"
427
+
428
+ def mark_inactive_for_adapter(self, adapter_name: str) -> list[str]:
429
+ """Mark every live value owned by the named adapter as inactive
430
+ (§120). Returns the affected names so the caller can disable
431
+ dependent handlers."""
432
+ affected: list[str] = []
433
+ for entry in self._entries.values():
434
+ if entry.adapter_name == adapter_name and entry.status != "inactive":
435
+ entry.status = "inactive"
436
+ affected.append(entry.name)
437
+ return affected
438
+
439
+ def is_unset(self, name: str) -> bool:
440
+ """True if `name` is a declared live value with no value yet —
441
+ conditions involving it evaluate as false per §113."""
442
+ entry = self._entries.get(name)
443
+ return entry is not None and entry.status == "unset"
444
+
445
+ def __contains__(self, name: str) -> bool:
446
+ return name in self._entries
447
+
448
+ def __len__(self) -> int:
449
+ return len(self._entries)