godcode-engine 4.0.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.
godcode/interpreter.py ADDED
@@ -0,0 +1,1104 @@
1
+ """Tree-walking interpreter for God Code v2.0.
2
+
3
+ Walks the AST produced by godcode.parser, evaluating statements in
4
+ lexically scoped environments. Divine-flavored messages on the surface,
5
+ real semantics underneath: lexical scoping, rite calls with RETURN,
6
+ a 100,000-iteration guard on WHILE, and line-numbered errors.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import random
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any, Callable, Optional
16
+
17
+ from godcode.ast import (
18
+ Anoint,
19
+ Ascend,
20
+ BinaryOp,
21
+ Bless,
22
+ Breathe,
23
+ CallExpr,
24
+ CreationBlock,
25
+ Declare,
26
+ DeclareIntent,
27
+ DefineRite,
28
+ ExprStmt,
29
+ ForLoop,
30
+ Identifier,
31
+ IfStmt,
32
+ Import,
33
+ Index,
34
+ ListLiteral,
35
+ Literal,
36
+ Program,
37
+ Prophesy,
38
+ Reflect,
39
+ Return,
40
+ Reveal,
41
+ SealStmt,
42
+ Testify,
43
+ UnaryOp,
44
+ WhileLoop,
45
+ )
46
+ from godcode.environment import Environment
47
+ from godcode.errors import AscendSignal, GodCodeError, GodRuntimeError, ReturnSignal
48
+ from godcode.lexer import Lexer
49
+ from godcode.parser import Parser
50
+ from godcode import plugins
51
+ from godcode import chain as chain_module
52
+ from godcode.values import Contract, RiteFunction, Symbol
53
+
54
+ _WHILE_ITERATION_CAP = 100_000
55
+
56
+
57
+ class Interpreter:
58
+ """Walks the God Code AST and brings it to life."""
59
+
60
+ def __init__(
61
+ self,
62
+ spirit=None,
63
+ ledger=None,
64
+ log_path: str | None = "logs/godcode.log",
65
+ interactive: bool = False,
66
+ chain_adapters: dict | None = None,
67
+ ):
68
+ self.spirit = spirit
69
+ self.ledger = ledger
70
+ self.log_path = log_path
71
+ self.interactive = interactive
72
+ self.output: list[str] = [] # every REVEAL line, in order
73
+ self.emit: Callable[[str], None] = print # REVEAL output sink; override to capture
74
+ self.last_value: Any = None # value of the last expression statement (embedding API)
75
+ self.env = Environment() # root environment
76
+ self.source_dir = Path.cwd() # for IMPORT resolution
77
+ self._imported: set[str] = set() # resolved scroll paths already run
78
+ self._import_stack: list[str] = [] # scrolls currently being run
79
+ self._last_source: str = ""
80
+ self._log_handle: Any = None
81
+ self._plugin_verbs: dict[str, Callable[..., Any]] = {} # namespaced verbs from plugins
82
+ self._plugin_verb_info: dict[str, dict] = {} # name -> {"plugin", "trusted", "func"}
83
+ self.loaded_plugins: list[str] = [] # plugin names whose register() ran cleanly
84
+ # --- v4.0: intent layer + chain adapters ---
85
+ self.intents: dict[str, str] = {} # rite name -> declared intent text
86
+ self.intent_checks: list[dict] = [] # per-invocation alignment records
87
+ self.chain_adapters = (
88
+ chain_adapters
89
+ if chain_adapters is not None
90
+ else chain_module.default_adapters()
91
+ )
92
+ # --- end v4.0 ---
93
+ self._builtins: dict[str, Callable[..., Any]] = {
94
+ "LEN": self._builtin_len,
95
+ "STR": self._builtin_str,
96
+ "NUM": self._builtin_num,
97
+ "TYPE": self._builtin_type,
98
+ "RANDOM": self._builtin_random,
99
+ "RANGE": self._builtin_range,
100
+ "PUSH": self._builtin_push,
101
+ "UPPER": self._builtin_upper,
102
+ "LOWER": self._builtin_lower,
103
+ "SPLIT": self._builtin_split,
104
+ "JOIN": self._builtin_join,
105
+ "ASK": self._builtin_ask,
106
+ "BEHOLD": self._builtin_behold,
107
+ "REVERSE": self._builtin_reverse,
108
+ "SUMMON": self._builtin_summon,
109
+ "ANCHOR": self._builtin_anchor, # v4.0
110
+ "CONSULT": self._builtin_consult, # v4.0
111
+ }
112
+ # Pillar 3 — plugins auto-load at startup (the same path `godcode run`
113
+ # and the REPL take). GODCODE_NO_PLUGINS=1 disables this.
114
+ if os.environ.get(plugins.DISABLE_ENV_VAR) != "1":
115
+ self.loaded_plugins = plugins.load_plugins(self)
116
+
117
+ # ------------------------------------------------- plugin verb registry
118
+
119
+ def register_plugin_verb(
120
+ self,
121
+ name: str,
122
+ func: Callable[..., Any],
123
+ *,
124
+ plugin: str | None = None,
125
+ ) -> str:
126
+ """Register a plugin verb callable from God Code via SUMMON.
127
+
128
+ *name* is namespaced by convention (``"clockwork.now"``); *func* is a
129
+ plain Python callable ``func(*args)`` — values are converted
130
+ God Code <-> Python automatically (see godcode.plugins). The verb is
131
+ marked ``trusted=True``: plugin code is trusted host code and SUMMON
132
+ calls bypass sandbox policy by design; the marker lets a future
133
+ sandbox pillar consult it.
134
+ """
135
+ if not isinstance(name, str) or not name:
136
+ raise ValueError("a plugin verb needs a non-empty string name")
137
+ if not callable(func):
138
+ raise ValueError(f"plugin verb '{name}' is not callable")
139
+ adapter = plugins.make_verb_adapter(name, func)
140
+ self._builtins[name] = adapter # exact key — namespaced names keep their case
141
+ self._plugin_verbs[name] = adapter
142
+ self._plugin_verb_info[name] = {"plugin": plugin, "trusted": True, "func": func}
143
+ return name
144
+
145
+ @property
146
+ def plugin_verb_info(self) -> dict[str, dict]:
147
+ """Read-only view of plugin verb metadata (name -> plugin/trusted/func)."""
148
+ return dict(self._plugin_verb_info)
149
+
150
+ # ------------------------------------------------------------------ run
151
+
152
+ def run(self, program, source_name: str = "<creation>") -> None:
153
+ """Execute a parsed Program. ASCEND ends the run in peace."""
154
+ self._imported = set()
155
+ self._import_stack = []
156
+ if self._looks_like_file(source_name):
157
+ self.source_dir = Path(source_name).resolve().parent
158
+ self._open_log()
159
+ try:
160
+ self._log(f"SESSION BEGIN {source_name} {self._now()}")
161
+ try:
162
+ self._exec_block(self._statements_of(program), self.env)
163
+ except AscendSignal:
164
+ message = "🕊 Creation ascended in peace."
165
+ print(message)
166
+ self._log("ASCEND :: the creation ascended in peace")
167
+ self._log(f"SESSION END {source_name}")
168
+ finally:
169
+ self._close_log()
170
+
171
+ def run_source(self, source: str, source_name: str = "<creation>") -> None:
172
+ """Lex, parse, and run God Code source text."""
173
+ self._last_source = source
174
+ tokens = Lexer(source).lex()
175
+ program = Parser(tokens).parse()
176
+ self.run(program, source_name=source_name)
177
+
178
+ @staticmethod
179
+ def _statements_of(program):
180
+ statements = getattr(program, "statements", None)
181
+ if statements is None:
182
+ raise GodRuntimeError("What was given to run is not a creation.")
183
+ return statements
184
+
185
+ @staticmethod
186
+ def _looks_like_file(source_name: str) -> bool:
187
+ try:
188
+ return Path(source_name).is_file()
189
+ except (OSError, ValueError):
190
+ return False
191
+
192
+ # ------------------------------------------------------------- statements
193
+
194
+ def _exec_block(self, statements: list, env: Environment) -> None:
195
+ for stmt in statements:
196
+ self._log_statement(stmt)
197
+ try:
198
+ self._exec_stmt(stmt, env)
199
+ except (ReturnSignal, AscendSignal):
200
+ raise # control-flow signals pass through untouched
201
+ except GodCodeError as err:
202
+ self._attach_line(err, getattr(stmt, "line", None))
203
+ self._log(f"ERROR :: line {getattr(err, 'line', '?')} :: {err}")
204
+ raise
205
+
206
+ @staticmethod
207
+ def _attach_line(err: GodCodeError, line) -> None:
208
+ try:
209
+ if getattr(err, "line", None) is None and line is not None:
210
+ err.line = line
211
+ except (AttributeError, TypeError):
212
+ pass
213
+
214
+ def _exec_stmt(self, stmt, env: Environment) -> None:
215
+ if isinstance(stmt, CreationBlock):
216
+ self._exec_block(stmt.statements, env)
217
+ elif isinstance(stmt, Declare):
218
+ env.define(stmt.name, self._eval_expr(stmt.value, env))
219
+ elif isinstance(stmt, DeclareIntent):
220
+ self._exec_declare_intent(stmt)
221
+ elif isinstance(stmt, Breathe):
222
+ self._exec_breathe(stmt, env)
223
+ elif isinstance(stmt, Reveal):
224
+ line = self.stringify(self._eval_expr(stmt.expr, env))
225
+ self.emit(line)
226
+ self.output.append(line)
227
+ elif isinstance(stmt, Prophesy):
228
+ self._exec_prophesy(stmt)
229
+ elif isinstance(stmt, Ascend):
230
+ raise AscendSignal()
231
+ elif isinstance(stmt, Reflect):
232
+ self._exec_reflect(env)
233
+ elif isinstance(stmt, Bless):
234
+ self._exec_consecrate(stmt, env, kind="bless")
235
+ elif isinstance(stmt, Anoint):
236
+ self._exec_consecrate(stmt, env, kind="anoint")
237
+ elif isinstance(stmt, SealStmt):
238
+ self._exec_seal(stmt, env)
239
+ elif isinstance(stmt, Testify):
240
+ self._exec_testify(stmt, env)
241
+ elif isinstance(stmt, IfStmt):
242
+ branch = stmt.then_body if self._truthy(self._eval_expr(stmt.cond, env)) else stmt.else_body
243
+ self._exec_block(branch, env)
244
+ elif isinstance(stmt, ForLoop):
245
+ self._exec_for(stmt, env)
246
+ elif isinstance(stmt, WhileLoop):
247
+ self._exec_while(stmt, env)
248
+ elif isinstance(stmt, DefineRite):
249
+ env.define(stmt.name, RiteFunction(stmt.name, stmt.params, stmt.body, env))
250
+ elif isinstance(stmt, Return):
251
+ raise ReturnSignal(self._eval_expr(stmt.expr, env) if stmt.expr is not None else None)
252
+ elif isinstance(stmt, Import):
253
+ self._exec_import(stmt, env)
254
+ elif isinstance(stmt, ExprStmt):
255
+ # The value is kept for the embedding API (RunResult.return_value).
256
+ self.last_value = self._eval_expr(stmt.expr, env)
257
+ else:
258
+ raise GodRuntimeError(
259
+ f"The heavens do not recognize this utterance: {type(stmt).__name__}.",
260
+ getattr(stmt, "line", None),
261
+ )
262
+
263
+ # ------------------------------------------------------- statement helpers
264
+
265
+ def _exec_breathe(self, stmt: Breathe, env: Environment) -> None:
266
+ line = getattr(stmt, "line", None)
267
+ if not env.is_bound(stmt.name):
268
+ raise GodRuntimeError(
269
+ f"There is no '{stmt.name}' to breathe into — "
270
+ "it was never spoken into being.",
271
+ line,
272
+ )
273
+ target = env.get(stmt.name)
274
+ if isinstance(target, Contract):
275
+ target.alive = True
276
+ print(f"[BREATHE] Life breathed into {stmt.name} 🕊")
277
+
278
+ def _exec_prophesy(self, stmt: Prophesy) -> None:
279
+ text = stmt.text or ""
280
+ if self.spirit is None:
281
+ print(f"[PROPHESY] The Spirit is silent — no oracle is bound. ({text!r})")
282
+ return
283
+ payload = text if text else self._last_source
284
+ utterance = self.spirit.prophesy(payload)
285
+ print(utterance)
286
+
287
+ def _exec_reflect(self, env: Environment) -> dict:
288
+ table = {name: self.stringify(value) for name, value in env.items()}
289
+ for name, rendered in table.items():
290
+ print(f"{name} = {rendered}")
291
+ return table
292
+
293
+ def _exec_consecrate(self, stmt, env: Environment, kind: str) -> None:
294
+ line = getattr(stmt, "line", None)
295
+ name = stmt.name
296
+ verb = "bless" if kind == "bless" else "anoint"
297
+ if not env.is_bound(name):
298
+ raise GodRuntimeError(
299
+ f"There is no '{name}' to {verb} — it was never spoken into being.",
300
+ line,
301
+ )
302
+ target = env.get(name)
303
+ if isinstance(target, Contract):
304
+ if kind == "bless":
305
+ target.blessed = True
306
+ else:
307
+ target.anointed = True
308
+ mark = "✨" if kind == "bless" else "🕊"
309
+ print(f"[{kind.upper()}] {name} is {verb}ed {mark}")
310
+
311
+ def _exec_seal(self, stmt: SealStmt, env: Environment) -> None:
312
+ value = self._eval_expr(stmt.expr, env)
313
+ record: dict[str, Any] = {
314
+ "sealed": self.stringify(value),
315
+ "type": self.type_name(value),
316
+ "by": "godcode",
317
+ }
318
+ if isinstance(value, Contract):
319
+ record.update(
320
+ {
321
+ "name": value.name,
322
+ "alive": value.alive,
323
+ "blessed": value.blessed,
324
+ "anointed": value.anointed,
325
+ }
326
+ )
327
+ if self.ledger is None:
328
+ print("[SEAL] ⚠ No covenant ledger is bound — the seal is spoken but not recorded.")
329
+ self._log("SEAL :: no ledger bound; seal spoken but not recorded")
330
+ return
331
+ block = self.ledger.seal(record)
332
+ digest = str(block.get("hash", ""))[:8]
333
+ print(f"[SEAL] Covenant sealed · block {block.get('index')} · {digest} 🔒")
334
+ self._log(f"SEAL :: block {block.get('index')} recorded")
335
+
336
+ def _exec_testify(self, stmt: Testify, env: Environment) -> None:
337
+ if self._truthy(self._eval_expr(stmt.expr, env)):
338
+ print("[TESTIFY] It is true. ✝")
339
+ else:
340
+ raise GodRuntimeError(
341
+ "The testimony has failed — what was spoken does not hold true.",
342
+ getattr(stmt, "line", None),
343
+ )
344
+
345
+ # ------------------------------------------------- v4.0: intent layer
346
+
347
+ def _exec_declare_intent(self, stmt: DeclareIntent) -> None:
348
+ """Register a natural-language intent on a named rite."""
349
+ self.intents[stmt.rite] = stmt.text
350
+ if self.spirit is not None:
351
+ self.spirit.declare_intent(stmt.rite, stmt.text)
352
+ print(f'[INTENT] {stmt.rite} now carries the intent: "{stmt.text}" 🕊')
353
+ self._log(f"INTENT DECLARE :: {stmt.rite}")
354
+
355
+ def _discern_intent(self, rite: RiteFunction, declared: str) -> None:
356
+ """Discern a rite's actual intent and counsel on divergence.
357
+
358
+ Classifies the rite's own words with the Spirit Engine and
359
+ compares against the declared intent. A gentle [INTENT] notice
360
+ when they align, a [WARNING] when they drift. The run is never
361
+ failed over divergence: the Spirit counsels, it does not condemn.
362
+ """
363
+ from godcode.cli import CanonicalFormatter # lazy: cli imports us lazily
364
+
365
+ body_text = CanonicalFormatter().format(Program(statements=rite.body))
366
+ discerned = self.spirit.classify(body_text)
367
+ aligned = self.spirit.intents_aligned(declared, discerned)
368
+ self.intent_checks.append(
369
+ {
370
+ "rite": rite.name,
371
+ "declared": declared,
372
+ "discerned": discerned["intent"],
373
+ "confidence": discerned["confidence"],
374
+ "aligned": aligned,
375
+ }
376
+ )
377
+ if aligned:
378
+ print(f'[INTENT] {rite.name} walks in its declared intent: "{declared}" 🕊')
379
+ else:
380
+ print(
381
+ f'[WARNING] {rite.name} drifts from its declared intent. '
382
+ f'Declared: "{declared}". '
383
+ f'Discerned: "{discerned["intent"]}". '
384
+ "The Spirit counsels; it does not condemn."
385
+ )
386
+ self._log(f"INTENT CHECK :: {rite.name} aligned={aligned}")
387
+
388
+ def _exec_for(self, stmt: ForLoop, env: Environment) -> None:
389
+ line = getattr(stmt, "line", None)
390
+ iterable = self._eval_expr(stmt.iterable, env)
391
+ if isinstance(iterable, Symbol):
392
+ raise GodRuntimeError(
393
+ f"FOR needs a list or a word to walk through, not the bare spirit '{iterable}'.",
394
+ line,
395
+ )
396
+ if isinstance(iterable, str):
397
+ items = list(iterable)
398
+ elif isinstance(iterable, list):
399
+ items = list(iterable)
400
+ else:
401
+ raise GodRuntimeError(
402
+ f"FOR cannot walk through {self.type_name(iterable)} — only lists and words.",
403
+ line,
404
+ )
405
+ child = Environment(parent=env) # one child env for the whole loop
406
+ for item in items:
407
+ child.define(stmt.var, item)
408
+ self._exec_block(stmt.body, child)
409
+
410
+ def _exec_while(self, stmt: WhileLoop, env: Environment) -> None:
411
+ line = getattr(stmt, "line", None)
412
+ count = 0
413
+ # The body runs in the current environment so DECLARE can rebind the
414
+ # names the condition watches (there is no separate assignment rite).
415
+ while self._truthy(self._eval_expr(stmt.cond, env)):
416
+ count += 1
417
+ if count > _WHILE_ITERATION_CAP:
418
+ raise GodRuntimeError(
419
+ "The cycle is endless — 100,000 turns and still no rest. "
420
+ "The loop is released.",
421
+ line,
422
+ )
423
+ self._exec_block(stmt.body, env)
424
+
425
+ def _exec_import(self, stmt: Import, env: Environment) -> None:
426
+ line = getattr(stmt, "line", None)
427
+ path = self._resolve_import(stmt.path, line)
428
+ key = str(path)
429
+ if key in self._imported:
430
+ return # already breathed in; skip
431
+ if key in self._import_stack:
432
+ raise GodRuntimeError(
433
+ f"The scroll '{stmt.path}' calls upon itself — a circle with no end.",
434
+ line,
435
+ )
436
+ self._imported.add(key)
437
+ self._import_stack.append(key)
438
+ previous_dir = self.source_dir
439
+ previous_source = self._last_source
440
+ self.source_dir = path.parent
441
+ try:
442
+ source = path.read_text(encoding="utf-8")
443
+ self._last_source = source
444
+ program = Parser(Lexer(source).lex()).parse()
445
+ self._log(f"IMPORT :: {path}")
446
+ self._exec_block(self._statements_of(program), env)
447
+ finally:
448
+ self._import_stack.pop()
449
+ self.source_dir = previous_dir
450
+ self._last_source = previous_source
451
+
452
+ def _resolve_import(self, import_path: str, line) -> Path:
453
+ raw = Path(import_path)
454
+ scrolls_dir = Path(__file__).parent / "scrolls"
455
+ candidates = [
456
+ self.source_dir / raw,
457
+ Path.cwd() / raw,
458
+ scrolls_dir / raw,
459
+ ]
460
+ if not raw.suffix:
461
+ candidates.extend(
462
+ [
463
+ self.source_dir / f"{import_path}.god",
464
+ Path.cwd() / f"{import_path}.god",
465
+ scrolls_dir / f"{import_path}.god",
466
+ ]
467
+ )
468
+ for candidate in candidates:
469
+ try:
470
+ if candidate.is_file():
471
+ return candidate.resolve()
472
+ except OSError:
473
+ continue
474
+ # --- v3: scroll registry --- installed registry scrolls
475
+ # (project-local .godcode/scrolls, then user-global ~/.godcode/scrolls)
476
+ # resolve bare names via their manifest entry file. Stdlib keeps its
477
+ # precedence above, so stdlib behavior is unchanged.
478
+ installed = self._resolve_installed_scroll(import_path)
479
+ if installed is not None:
480
+ return installed
481
+ # --- end v3: scroll registry ---
482
+ raise GodRuntimeError(
483
+ f"The scroll '{import_path}' could not be found — not beside the "
484
+ "creation, not in this place, not among the scrolls.",
485
+ line,
486
+ )
487
+
488
+ def _resolve_installed_scroll(self, import_path: str) -> Path | None:
489
+ """Resolve a bare scroll name against installed registry scrolls."""
490
+ if "/" in import_path or "\\" in import_path:
491
+ return None # only bare names; never paths
492
+ name = import_path[:-4] if import_path.endswith(".god") else import_path
493
+ try:
494
+ from godcode.registry import ScrollRegistry
495
+ except ImportError:
496
+ return None
497
+ try:
498
+ return ScrollRegistry().resolve_entry(name)
499
+ except Exception:
500
+ return None
501
+
502
+ # ------------------------------------------------------------- expressions
503
+
504
+ def _eval_expr(self, expr, env: Environment):
505
+ line = getattr(expr, "line", None)
506
+ if isinstance(expr, Literal):
507
+ return expr.value
508
+ if isinstance(expr, Identifier):
509
+ return env.get(expr.name)
510
+ if isinstance(expr, ListLiteral):
511
+ return [self._eval_expr(item, env) for item in expr.items]
512
+ if isinstance(expr, Index):
513
+ return self._eval_index(expr, env)
514
+ if isinstance(expr, UnaryOp):
515
+ return self._eval_unary(expr, env)
516
+ if isinstance(expr, BinaryOp):
517
+ return self._eval_binary(expr, env)
518
+ if isinstance(expr, CallExpr):
519
+ args = [self._eval_expr(arg, env) for arg in expr.args]
520
+ return self._call(expr.callee, args, env, line)
521
+ raise GodRuntimeError(
522
+ f"The heavens do not recognize this expression: {type(expr).__name__}.",
523
+ line,
524
+ )
525
+
526
+ def _eval_index(self, expr: Index, env: Environment):
527
+ line = getattr(expr, "line", None)
528
+ obj = self._eval_expr(expr.obj, env)
529
+ index = self._eval_expr(expr.index, env)
530
+ # --- v4.0: maps are indexed by word ---
531
+ if isinstance(obj, dict):
532
+ if not isinstance(index, str):
533
+ raise GodRuntimeError(
534
+ f"Only words may point into a map — not {self.type_name(index)}.",
535
+ line,
536
+ )
537
+ key = str(index)
538
+ if key not in obj:
539
+ known = ", ".join(obj) or "it holds nothing"
540
+ raise GodRuntimeError(
541
+ f"The map holds no '{key}' — its keys are: {known}.",
542
+ line,
543
+ )
544
+ return obj[key]
545
+ # --- end v4.0 ---
546
+ if not self._is_int(index):
547
+ raise GodRuntimeError(
548
+ f"Only whole numbers may point into {self.type_name(obj)} — not {self.type_name(index)}.",
549
+ line,
550
+ )
551
+ if isinstance(obj, Symbol):
552
+ raise GodRuntimeError(
553
+ f"Cannot point into the bare spirit '{obj}' — bind it to a list or word first.",
554
+ line,
555
+ )
556
+ if isinstance(obj, (list, str)):
557
+ try:
558
+ return obj[index]
559
+ except IndexError:
560
+ raise GodRuntimeError(
561
+ f"Index {index} reaches beyond what is there "
562
+ f"(it holds {len(obj)}).",
563
+ line,
564
+ ) from None
565
+ raise GodRuntimeError(
566
+ f"Cannot point into {self.type_name(obj)} — only lists and words may be indexed.",
567
+ line,
568
+ )
569
+
570
+ def _eval_unary(self, expr: UnaryOp, env: Environment):
571
+ line = getattr(expr, "line", None)
572
+ operand = self._eval_expr(expr.operand, env)
573
+ if expr.op == "not":
574
+ return not self._truthy(operand)
575
+ if expr.op == "-":
576
+ if self._is_number(operand):
577
+ return -operand
578
+ raise GodRuntimeError(
579
+ f"Cannot negate {self.type_name(operand)} — only numbers know the void's mirror.",
580
+ line,
581
+ )
582
+ raise GodRuntimeError(f"Unknown sign '{expr.op}'.", line)
583
+
584
+ def _eval_binary(self, expr: BinaryOp, env: Environment):
585
+ line = getattr(expr, "line", None)
586
+ left = self._eval_expr(expr.left, env)
587
+ right = self._eval_expr(expr.right, env)
588
+ op = expr.op
589
+ if op == "==":
590
+ return self._equals(left, right)
591
+ if op == "!=":
592
+ return not self._equals(left, right)
593
+ if op in ("<", ">", "<=", ">="):
594
+ return self._compare(op, left, right, line)
595
+ if op == "+":
596
+ return self._add(left, right, line)
597
+ if op in ("-", "*", "/", "%"):
598
+ return self._arithmetic(op, left, right, line)
599
+ if op == "and":
600
+ return left if not self._truthy(left) else right
601
+ if op == "or":
602
+ return left if self._truthy(left) else right
603
+ raise GodRuntimeError(f"Unknown joining '{op}'.", line)
604
+
605
+ # ------------------------------------------------------- value operations
606
+
607
+ @staticmethod
608
+ def _is_int(value) -> bool:
609
+ return isinstance(value, int) and not isinstance(value, bool)
610
+
611
+ @classmethod
612
+ def _is_number(cls, value) -> bool:
613
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
614
+
615
+ def _equals(self, left, right) -> bool:
616
+ # Contract vs Contract: by name. Symbol vs str: by text (str subclass
617
+ # equality already does this). Numbers: cross-type. Lists: elementwise.
618
+ if isinstance(left, Contract) or isinstance(right, Contract):
619
+ return (
620
+ isinstance(left, Contract)
621
+ and isinstance(right, Contract)
622
+ and left.name == right.name
623
+ )
624
+ try:
625
+ return bool(left == right)
626
+ except Exception: # pragma: no cover - defensive
627
+ return False
628
+
629
+ def _compare(self, op: str, left, right, line) -> bool:
630
+ if self._is_number(left) and self._is_number(right):
631
+ if op == "<":
632
+ return left < right
633
+ if op == ">":
634
+ return left > right
635
+ if op == "<=":
636
+ return left <= right
637
+ return left >= right
638
+ raise GodRuntimeError(
639
+ f"Cannot weigh {self.type_name(left)} against {self.type_name(right)} — "
640
+ "only numbers may be measured.",
641
+ line,
642
+ )
643
+
644
+ def _add(self, left, right, line):
645
+ if self._is_number(left) and self._is_number(right):
646
+ return left + right
647
+ if isinstance(left, str) and isinstance(right, str):
648
+ return str(left) + str(right) # plain str, even for Symbols
649
+ if isinstance(left, list) and isinstance(right, list):
650
+ return left + right
651
+ raise GodRuntimeError(
652
+ f"Cannot join {self.type_name(left)} and {self.type_name(right)} — "
653
+ "they are of different kingdoms.",
654
+ line,
655
+ )
656
+
657
+ def _arithmetic(self, op: str, left, right, line):
658
+ if op == "%":
659
+ if not (self._is_int(left) and self._is_int(right)):
660
+ raise GodRuntimeError(
661
+ f"The remainder rite needs whole numbers, not "
662
+ f"{self.type_name(left)} and {self.type_name(right)}.",
663
+ line,
664
+ )
665
+ elif not (self._is_number(left) and self._is_number(right)):
666
+ raise GodRuntimeError(
667
+ f"Cannot reckon {self.type_name(left)} and {self.type_name(right)} — "
668
+ "only numbers may be reckoned.",
669
+ line,
670
+ )
671
+ if op in ("/", "%") and right == 0:
672
+ raise GodRuntimeError(
673
+ "Division by nothing is not permitted — even the heavens "
674
+ "cannot split the void.",
675
+ line,
676
+ )
677
+ if op == "-":
678
+ return left - right
679
+ if op == "*":
680
+ return left * right
681
+ if op == "/":
682
+ return left / right # always float, as the waters divide
683
+ return left % right
684
+
685
+ @staticmethod
686
+ def _truthy(value) -> bool:
687
+ # False / None / 0 / "" / [] / {} are empty; Symbols are always truthy.
688
+ if value is None:
689
+ return False
690
+ if isinstance(value, bool):
691
+ return value
692
+ if isinstance(value, Symbol):
693
+ return True
694
+ if isinstance(value, (int, float)):
695
+ return value != 0
696
+ if isinstance(value, str):
697
+ return len(value) > 0
698
+ if isinstance(value, (list, dict)):
699
+ return len(value) > 0
700
+ return True # Contracts, rites, and all other living things
701
+
702
+ # ----------------------------------------------------------------- rites
703
+
704
+ def _call(self, name: str, args: list, env: Environment, line):
705
+ if name == "contract":
706
+ if len(args) == 1 and isinstance(args[0], (str, Symbol)):
707
+ return Contract(str(args[0]))
708
+ raise GodRuntimeError(
709
+ "The contract rite needs exactly one name — a single word to seal.",
710
+ line,
711
+ )
712
+ target = env.get(name) if env.is_bound(name) else None
713
+ if isinstance(target, RiteFunction):
714
+ return self._call_rite(target, args, line)
715
+ # Exact match first so namespaced plugin verbs ("clockwork.now") keep
716
+ # their case; core verbs still resolve case-insensitively via UPPER.
717
+ builtin = self._builtins.get(name)
718
+ if builtin is None:
719
+ builtin = self._builtins.get(name.upper())
720
+ if builtin is not None:
721
+ return builtin(args, line)
722
+ if target is not None:
723
+ raise GodRuntimeError(
724
+ f"'{name}' is {self.type_name(target)}, not a rite — it cannot be invoked.",
725
+ line,
726
+ )
727
+ raise GodRuntimeError(
728
+ f"There is no rite named '{name}' — the heavens do not know it.",
729
+ line,
730
+ )
731
+
732
+ def _call_rite(self, rite: RiteFunction, args: list, line):
733
+ if len(args) != len(rite.params):
734
+ want, got = len(rite.params), len(args)
735
+ raise GodRuntimeError(
736
+ f"Rite '{rite.name}' asks for {want} offering{'s' if want != 1 else ''}, "
737
+ f"but {got} {'was' if got == 1 else 'were'} brought.",
738
+ line,
739
+ )
740
+ # --- v4.0: intent layer — discern the rite's actual intent when it
741
+ # carries a declared one. Never fails the run; counsel, don't punish.
742
+ declared = self.intents.get(rite.name)
743
+ if declared is not None and self.spirit is not None:
744
+ self._discern_intent(rite, declared)
745
+ # --- end v4.0 ---
746
+ call_env = Environment(parent=rite.closure_env)
747
+ for param, value in zip(rite.params, args):
748
+ call_env.define(param, value)
749
+ rendered = ", ".join(self.stringify(a) for a in args)
750
+ self._log(f"RITE CALL :: {rite.name}({rendered})")
751
+ try:
752
+ self._exec_block(rite.body, call_env)
753
+ except ReturnSignal as ret:
754
+ return ret.value
755
+ return None
756
+
757
+ # --------------------------------------------------------------- builtins
758
+
759
+ @staticmethod
760
+ def _arity(name: str, args: list, want, line) -> None:
761
+ ok = len(args) == want if isinstance(want, int) else len(args) in want
762
+ if not ok:
763
+ expected = want if isinstance(want, int) else " or ".join(map(str, want))
764
+ raise GodRuntimeError(
765
+ f"{name} asks for {expected} offering(s), but {len(args)} came.",
766
+ line,
767
+ )
768
+
769
+ def _builtin_len(self, args, line):
770
+ self._arity("LEN", args, 1, line)
771
+ value = args[0]
772
+ if isinstance(value, (str, list)):
773
+ return len(value)
774
+ raise GodRuntimeError(
775
+ f"LEN cannot measure {self.type_name(value)} — only words and lists have length.",
776
+ line,
777
+ )
778
+
779
+ def _builtin_str(self, args, line):
780
+ self._arity("STR", args, 1, line)
781
+ value = args[0]
782
+ if isinstance(value, str):
783
+ return str(value)
784
+ return self.stringify(value)
785
+
786
+ def _builtin_num(self, args, line):
787
+ self._arity("NUM", args, 1, line)
788
+ value = args[0]
789
+ if self._is_number(value):
790
+ return value
791
+ if isinstance(value, str):
792
+ text = value.strip()
793
+ try:
794
+ return int(text)
795
+ except ValueError:
796
+ pass
797
+ try:
798
+ return float(text)
799
+ except ValueError:
800
+ pass
801
+ raise GodRuntimeError(
802
+ f"NUM cannot number the word '{value}' — it holds no number.",
803
+ line,
804
+ )
805
+ raise GodRuntimeError(
806
+ f"NUM cannot number {self.type_name(value)}.",
807
+ line,
808
+ )
809
+
810
+ def _builtin_type(self, args, line):
811
+ self._arity("TYPE", args, 1, line)
812
+ return self.type_name(args[0])
813
+
814
+ def _builtin_random(self, args, line):
815
+ self._arity("RANDOM", args, 1, line)
816
+ bound = args[0]
817
+ if not self._is_int(bound) or bound <= 0:
818
+ raise GodRuntimeError(
819
+ "RANDOM needs a positive whole number to cast lots within.",
820
+ line,
821
+ )
822
+ return random.randrange(bound)
823
+
824
+ def _builtin_range(self, args, line):
825
+ self._arity("RANGE", args, (1, 2), line)
826
+ for value in args:
827
+ if not self._is_int(value):
828
+ raise GodRuntimeError(
829
+ "RANGE walks only in whole numbers.",
830
+ line,
831
+ )
832
+ if len(args) == 1:
833
+ if args[0] < 0:
834
+ raise GodRuntimeError("RANGE cannot walk a negative span.", line)
835
+ return list(range(args[0]))
836
+ return list(range(args[0], args[1]))
837
+
838
+ def _builtin_push(self, args, line):
839
+ self._arity("PUSH", args, 2, line)
840
+ items, value = args
841
+ if not isinstance(items, list):
842
+ raise GodRuntimeError(
843
+ f"PUSH needs a list to build upon, not {self.type_name(items)}.",
844
+ line,
845
+ )
846
+ return items + [value] # a new list; the old one is untouched
847
+
848
+ def _builtin_upper(self, args, line):
849
+ self._arity("UPPER", args, 1, line)
850
+ return self._upper_lower(args[0], str.upper, "UPPER", line)
851
+
852
+ def _builtin_lower(self, args, line):
853
+ self._arity("LOWER", args, 1, line)
854
+ return self._upper_lower(args[0], str.lower, "LOWER", line)
855
+
856
+ def _upper_lower(self, value, func, name, line):
857
+ if not isinstance(value, str):
858
+ raise GodRuntimeError(
859
+ f"{name} speaks only to words, not {self.type_name(value)}.",
860
+ line,
861
+ )
862
+ return func(str(value))
863
+
864
+ def _builtin_split(self, args, line):
865
+ self._arity("SPLIT", args, 2, line)
866
+ text, sep = args
867
+ if not isinstance(text, str) or not isinstance(sep, str):
868
+ raise GodRuntimeError("SPLIT needs two words — the text and the divider.", line)
869
+ return text.split(str(sep))
870
+
871
+ def _builtin_join(self, args, line):
872
+ self._arity("JOIN", args, 2, line)
873
+ items, sep = args
874
+ if not isinstance(items, list) or not isinstance(sep, str):
875
+ raise GodRuntimeError(
876
+ "JOIN needs a list and a word to bind it with.", line
877
+ )
878
+ return str(sep).join(self.stringify(item) for item in items)
879
+
880
+ def _builtin_ask(self, args, line):
881
+ self._arity("ASK", args, (0, 1), line)
882
+ prompt = self.stringify(args[0]) if args else ""
883
+ try:
884
+ answer = input(prompt)
885
+ except EOFError:
886
+ return ""
887
+ return answer
888
+
889
+ def _builtin_behold(self, args, line):
890
+ self._arity("BEHOLD", args, 0, line)
891
+ return datetime.now(timezone.utc).isoformat()
892
+
893
+ def _builtin_reverse(self, args, line):
894
+ self._arity("REVERSE", args, 1, line)
895
+ value = args[0]
896
+ if isinstance(value, str):
897
+ return str(value)[::-1]
898
+ if isinstance(value, list):
899
+ return value[::-1]
900
+ raise GodRuntimeError(
901
+ f"REVERSE can only turn back words and lists, not {self.type_name(value)}.",
902
+ line,
903
+ )
904
+
905
+ def _builtin_summon(self, args, line):
906
+ # Pillar 3 FFI: SUMMON("plugin.verb", arg1, ...) calls a
907
+ # plugin-registered verb with converted arguments. The name is a
908
+ # string literal so no grammar change was needed; namespaced names
909
+ # ("clockwork.now") keep plugin verbs from colliding with core rites.
910
+ if not args:
911
+ raise GodRuntimeError(
912
+ 'SUMMON needs a verb to call upon — SUMMON("name.verb", ...).',
913
+ line,
914
+ )
915
+ target = args[0]
916
+ if not isinstance(target, str):
917
+ raise GodRuntimeError(
918
+ "SUMMON needs the verb's name as a word, "
919
+ f"not {self.type_name(target)}.",
920
+ line,
921
+ )
922
+ verb = self._plugin_verbs.get(target)
923
+ if verb is None:
924
+ known = ", ".join(sorted(self._plugin_verbs)) or "none are present"
925
+ raise GodRuntimeError(
926
+ f"SUMMON knows no verb '{target}' — the summoned are: {known}.",
927
+ line,
928
+ )
929
+ return verb(args[1:], line)
930
+
931
+ # ------------------------------------------------- v4.0: chain + oracle
932
+
933
+ def _builtin_anchor(self, args, line):
934
+ """ANCHOR(expr [, chain_name]) -- anchor a value's hash on a chain.
935
+
936
+ Returns the receipt: a map {chain, anchor_hash, height, timestamp,
937
+ payload_hash}. The default chain is "simulated" (a local
938
+ tamper-evident JSONL chain); real chain adapters register by name.
939
+ """
940
+ import hashlib
941
+
942
+ self._arity("ANCHOR", args, (1, 2), line)
943
+ value = args[0]
944
+ chain_name = chain_module.DEFAULT_CHAIN_NAME
945
+ if len(args) == 2:
946
+ name_arg = args[1]
947
+ if not isinstance(name_arg, str):
948
+ raise GodRuntimeError(
949
+ "ANCHOR needs the chain's name as a word, "
950
+ f"not {self.type_name(name_arg)}.",
951
+ line,
952
+ )
953
+ chain_name = str(name_arg)
954
+ adapter = self.chain_adapters.get(chain_name)
955
+ if adapter is None:
956
+ known = ", ".join(sorted(self.chain_adapters)) or "none are bound"
957
+ raise GodRuntimeError(
958
+ f"The chain '{chain_name}' is unknown to the heavens — "
959
+ f"the known chains are: {known}.",
960
+ line,
961
+ )
962
+ payload_hash = hashlib.sha256(
963
+ self.stringify(value).encode("utf-8")
964
+ ).hexdigest()
965
+ receipt = adapter.anchor(payload_hash)
966
+ digest = str(receipt.get("anchor_hash", ""))[:8]
967
+ print(
968
+ f"[ANCHOR] Anchored on {adapter.name} · height {receipt.get('height')} "
969
+ f"· {digest} ⚓"
970
+ )
971
+ self._log(f"ANCHOR :: {adapter.name} height {receipt.get('height')}")
972
+ return receipt
973
+
974
+ def _builtin_consult(self, args, line):
975
+ """CONSULT("question...") -- ask the local Spirit oracle for counsel.
976
+
977
+ Returns 2-3 sentences in the voice of the Spirit Engine's prophesy.
978
+ No external calls: the oracle is the local engine, or a gentle
979
+ silence when none is bound.
980
+ """
981
+ self._arity("CONSULT", args, 1, line)
982
+ question = args[0]
983
+ if not isinstance(question, str):
984
+ raise GodRuntimeError(
985
+ "CONSULT needs a question as a word, "
986
+ f"not {self.type_name(question)}.",
987
+ line,
988
+ )
989
+ if self.spirit is None:
990
+ return "The Spirit is silent on this question. Breathe, and ask again."
991
+ return self.spirit.counsel(str(question))
992
+
993
+ # ------------------------------------------------------- display & typing
994
+
995
+ def stringify(self, value) -> str:
996
+ """Render a God Code value as it appears in REVEAL."""
997
+ if value is None:
998
+ return "void"
999
+ if isinstance(value, bool):
1000
+ return "true" if value else "false"
1001
+ if isinstance(value, Symbol):
1002
+ return str(value)
1003
+ if isinstance(value, Contract):
1004
+ return str(value)
1005
+ if isinstance(value, RiteFunction):
1006
+ return f"rite {value.name}"
1007
+ if isinstance(value, list):
1008
+ return "[" + ", ".join(self.stringify(item) for item in value) + "]"
1009
+ if isinstance(value, dict):
1010
+ # v4.0: maps (e.g. anchor receipts) reveal as {key: value, ...}.
1011
+ inner = ", ".join(
1012
+ f"{key}: {self.stringify(item)}" for key, item in value.items()
1013
+ )
1014
+ return "{" + inner + "}"
1015
+ if isinstance(value, float) and value.is_integer():
1016
+ return str(int(value))
1017
+ if isinstance(value, float) and abs(value - round(value)) < 1e-9:
1018
+ # Near-integers (e.g. Newton's method settling on 12.00000001)
1019
+ # are revealed cleanly; full precision lives on in the value.
1020
+ return str(int(round(value)))
1021
+ return str(value)
1022
+
1023
+ def type_name(self, value) -> str:
1024
+ """The TYPE() name for a value: number/string/symbol/list/map/contract/rite/boolean/void."""
1025
+ if isinstance(value, bool):
1026
+ return "boolean"
1027
+ if value is None:
1028
+ return "void"
1029
+ if isinstance(value, Symbol):
1030
+ return "symbol"
1031
+ if isinstance(value, str):
1032
+ return "string"
1033
+ if isinstance(value, (int, float)):
1034
+ return "number"
1035
+ if isinstance(value, list):
1036
+ return "list"
1037
+ if isinstance(value, dict):
1038
+ return "map" # v4.0
1039
+ if isinstance(value, Contract):
1040
+ return "contract"
1041
+ if isinstance(value, RiteFunction):
1042
+ return "rite"
1043
+ return type(value).__name__.lower()
1044
+
1045
+ # ------------------------------------------------------------ audit log
1046
+
1047
+ @staticmethod
1048
+ def _now() -> str:
1049
+ return datetime.now(timezone.utc).isoformat()
1050
+
1051
+ def _open_log(self) -> None:
1052
+ if not self.log_path:
1053
+ return
1054
+ try:
1055
+ path = Path(self.log_path)
1056
+ if path.parent != Path("."):
1057
+ path.parent.mkdir(parents=True, exist_ok=True)
1058
+ self._log_handle = path.open("a", encoding="utf-8")
1059
+ except OSError:
1060
+ self._log_handle = None
1061
+
1062
+ def _close_log(self) -> None:
1063
+ handle, self._log_handle = self._log_handle, None
1064
+ if handle is not None:
1065
+ try:
1066
+ handle.close()
1067
+ except OSError:
1068
+ pass
1069
+
1070
+ def _log(self, message: str) -> None:
1071
+ if self._log_handle is None:
1072
+ return
1073
+ try:
1074
+ self._log_handle.write(f"[{self._now()}] {message}\n")
1075
+ self._log_handle.flush()
1076
+ except OSError:
1077
+ pass
1078
+
1079
+ def _log_statement(self, stmt) -> None:
1080
+ kind = type(stmt).__name__
1081
+ line = getattr(stmt, "line", "?")
1082
+ summary = self._summarize(stmt)
1083
+ self._log(f"{line} :: {kind} :: {summary}")
1084
+
1085
+ @staticmethod
1086
+ def _summarize(stmt) -> str:
1087
+ name = getattr(stmt, "name", "")
1088
+ extra = ""
1089
+ if isinstance(stmt, Declare):
1090
+ extra = f" {stmt.name}"
1091
+ elif isinstance(stmt, DeclareIntent):
1092
+ extra = f" {stmt.rite}"
1093
+ elif isinstance(stmt, (Breathe, Bless, Anoint)):
1094
+ extra = f" {stmt.name}"
1095
+ elif isinstance(stmt, DefineRite):
1096
+ extra = f" {stmt.name}"
1097
+ elif isinstance(stmt, ForLoop):
1098
+ extra = f" {stmt.var}"
1099
+ elif isinstance(stmt, Import):
1100
+ extra = f" {stmt.path}"
1101
+ elif isinstance(stmt, Prophesy):
1102
+ text = (stmt.text or "")[:40]
1103
+ extra = f" {text!r}" if text else ""
1104
+ return f"{type(stmt).__name__.upper()}{extra}"