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/__init__.py +86 -0
- godcode/__main__.py +7 -0
- godcode/agentics.py +235 -0
- godcode/ast.py +227 -0
- godcode/chain.py +210 -0
- godcode/cli.py +698 -0
- godcode/environment.py +74 -0
- godcode/errors.py +69 -0
- godcode/interpreter.py +1104 -0
- godcode/ledger.py +88 -0
- godcode/lexer.py +218 -0
- godcode/lsp.py +677 -0
- godcode/parser.py +525 -0
- godcode/plugins.py +255 -0
- godcode/registry.py +515 -0
- godcode/sandbox.py +385 -0
- godcode/scrolls/covenant.god +10 -0
- godcode/scrolls/covenant.toml +6 -0
- godcode/scrolls/lists.god +53 -0
- godcode/scrolls/lists.toml +6 -0
- godcode/scrolls/math.god +64 -0
- godcode/scrolls/math.toml +6 -0
- godcode/scrolls/prophecy.god +14 -0
- godcode/scrolls/prophecy.toml +6 -0
- godcode/scrolls/strings.god +32 -0
- godcode/scrolls/strings.toml +6 -0
- godcode/scrolls/time.god +10 -0
- godcode/scrolls/time.toml +6 -0
- godcode/spirit.py +180 -0
- godcode/tokens.py +91 -0
- godcode/tools.py +378 -0
- godcode/values.py +56 -0
- godcode_engine-4.0.0.dist-info/METADATA +212 -0
- godcode_engine-4.0.0.dist-info/RECORD +38 -0
- godcode_engine-4.0.0.dist-info/WHEEL +5 -0
- godcode_engine-4.0.0.dist-info/entry_points.txt +2 -0
- godcode_engine-4.0.0.dist-info/licenses/LICENSE +201 -0
- godcode_engine-4.0.0.dist-info/top_level.txt +1 -0
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}"
|