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/lsp.py
ADDED
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"""Language server for God Code (v3.0, Pillar 4).
|
|
2
|
+
|
|
3
|
+
A minimal Language Server Protocol implementation over stdio, hand-rolled
|
|
4
|
+
on top of the stdlib — no third-party dependencies. JSON-RPC 2.0 messages
|
|
5
|
+
are framed with ``Content-Length`` headers, per the LSP base protocol.
|
|
6
|
+
|
|
7
|
+
Lifecycle::
|
|
8
|
+
|
|
9
|
+
godcode lsp # start serving on stdin/stdout
|
|
10
|
+
|
|
11
|
+
Diagnostics are produced by the real lexer/parser pipeline (the same
|
|
12
|
+
logic ``godcode check`` uses): opening or changing a ``.god`` scroll
|
|
13
|
+
re-parses it and publishes the divine errors as LSP diagnostics.
|
|
14
|
+
|
|
15
|
+
Protocol notes:
|
|
16
|
+
* All logging goes to stderr. stdout is the protocol channel — nothing
|
|
17
|
+
else may ever be written there.
|
|
18
|
+
* Unknown methods answer with JSON-RPC error -32601 ("Method not found").
|
|
19
|
+
* Malformed frames are skipped; the server keeps serving.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Divine documentation table (hover + completion)
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
LSP_DOCS: dict[str, str] = {
|
|
33
|
+
# -- block sentinels -------------------------------------------------
|
|
34
|
+
"BEGIN CREATION": (
|
|
35
|
+
"**BEGIN CREATION**\n\n"
|
|
36
|
+
"Speak, and let there be a program. Every scroll opens its works "
|
|
37
|
+
"with `BEGIN CREATION` and closes them with `END CREATION`.\n\n"
|
|
38
|
+
"```godcode\nBEGIN CREATION\n REVEAL(\"Let there be light\")\n"
|
|
39
|
+
" ASCEND\nEND CREATION\n```"
|
|
40
|
+
),
|
|
41
|
+
"END CREATION": (
|
|
42
|
+
"**END CREATION**\n\n"
|
|
43
|
+
"The seal upon the scroll — it closes what `BEGIN CREATION` opened. "
|
|
44
|
+
"No works may follow it.\n\n"
|
|
45
|
+
"```godcode\nEND CREATION\n```"
|
|
46
|
+
),
|
|
47
|
+
"DEFINE RITE": (
|
|
48
|
+
"**DEFINE RITE**\n\n"
|
|
49
|
+
"Establish a rite — a named ceremony of statements that may be "
|
|
50
|
+
"invoked again and again. Closed with `END RITE`; a rite may "
|
|
51
|
+
"`RETURN` a value to its caller.\n\n"
|
|
52
|
+
"```godcode\nDEFINE RITE bless(name)\n"
|
|
53
|
+
" REVEAL(\"Blessed be \" + name)\nEND RITE\n```"
|
|
54
|
+
),
|
|
55
|
+
"END RITE": (
|
|
56
|
+
"**END RITE**\n\n"
|
|
57
|
+
"The closing of a rite begun with `DEFINE RITE`. All ceremonies "
|
|
58
|
+
"must be sealed.\n\n"
|
|
59
|
+
"```godcode\nEND RITE\n```"
|
|
60
|
+
),
|
|
61
|
+
"BREATHE LIFE INTO": (
|
|
62
|
+
"**BREATHE LIFE INTO**\n\n"
|
|
63
|
+
"Awaken a declared vessel so it may be used. A name must be "
|
|
64
|
+
"`DECLARE`d before the breath is given.\n\n"
|
|
65
|
+
"```godcode\nDECLARE vessel AS 0\nBREATHE LIFE INTO vessel\n```"
|
|
66
|
+
),
|
|
67
|
+
# -- keywords --------------------------------------------------------
|
|
68
|
+
"BEGIN": (
|
|
69
|
+
"**BEGIN**\n\n"
|
|
70
|
+
"The opening word of creation — always paired with `CREATION`. "
|
|
71
|
+
"See `BEGIN CREATION`.\n\n```godcode\nBEGIN CREATION\n```"
|
|
72
|
+
),
|
|
73
|
+
"CREATION": (
|
|
74
|
+
"**CREATION**\n\n"
|
|
75
|
+
"The body of all works — paired with `BEGIN` to open a scroll and "
|
|
76
|
+
"with `END` to close it. See `BEGIN CREATION`.\n\n"
|
|
77
|
+
"```godcode\nBEGIN CREATION\n ASCEND\nEND CREATION\n```"
|
|
78
|
+
),
|
|
79
|
+
"DECLARE": (
|
|
80
|
+
"**DECLARE**\n\n"
|
|
81
|
+
"Bring a name into being and bind it to a value. "
|
|
82
|
+
"Every vessel must be declared before the breath is given.\n\n"
|
|
83
|
+
"```godcode\nDECLARE tribes AS 12\n```"
|
|
84
|
+
),
|
|
85
|
+
"AS": (
|
|
86
|
+
"**AS**\n\n"
|
|
87
|
+
"The binding word — it joins a declared name to its value, as "
|
|
88
|
+
"in `DECLARE x AS 1`.\n\n```godcode\nDECLARE x AS 1\n```"
|
|
89
|
+
),
|
|
90
|
+
"IF": (
|
|
91
|
+
"**IF**\n\n"
|
|
92
|
+
"Weigh a condition; if it holds true, the words between `THEN` "
|
|
93
|
+
"and `ENDIF` come to pass.\n\n"
|
|
94
|
+
"```godcode\nIF faith > fear THEN\n REVEAL(\"walk on\")\nENDIF\n```"
|
|
95
|
+
),
|
|
96
|
+
"THEN": (
|
|
97
|
+
"**THEN**\n\n"
|
|
98
|
+
"Marks the start of the true branch after an `IF` condition. "
|
|
99
|
+
"Closed by `ENDIF` (or `ELSE`).\n\n```godcode\nIF x THEN\n REVEAL(x)\n"
|
|
100
|
+
"ENDIF\n```"
|
|
101
|
+
),
|
|
102
|
+
"ELSE": (
|
|
103
|
+
"**ELSE**\n\n"
|
|
104
|
+
"The other path — when the `IF` condition proves false, these "
|
|
105
|
+
"words come to pass instead.\n\n"
|
|
106
|
+
"```godcode\nIF x THEN\n REVEAL(\"yes\")\nELSE\n"
|
|
107
|
+
" REVEAL(\"no\")\nENDIF\n```"
|
|
108
|
+
),
|
|
109
|
+
"ENDIF": (
|
|
110
|
+
"**ENDIF**\n\n"
|
|
111
|
+
"The seal upon an `IF` weighing. Every `IF` must be closed.\n\n"
|
|
112
|
+
"```godcode\nENDIF\n```"
|
|
113
|
+
),
|
|
114
|
+
"FOR": (
|
|
115
|
+
"**FOR**\n\n"
|
|
116
|
+
"Walk through every member of a list or range, naming each one "
|
|
117
|
+
"in turn, until `ENDFOR`.\n\n"
|
|
118
|
+
"```godcode\nFOR tribe IN RANGE(12)\n REVEAL(tribe)\nENDFOR\n```"
|
|
119
|
+
),
|
|
120
|
+
"IN": (
|
|
121
|
+
"**IN**\n\n"
|
|
122
|
+
"The walking word — `FOR name IN list` binds each member in turn.\n\n"
|
|
123
|
+
"```godcode\nFOR star IN heavens\n REVEAL(star)\nENDFOR\n```"
|
|
124
|
+
),
|
|
125
|
+
"ENDFOR": (
|
|
126
|
+
"**ENDFOR**\n\n"
|
|
127
|
+
"The seal upon a `FOR` walk. Every journey must end.\n\n"
|
|
128
|
+
"```godcode\nENDFOR\n```"
|
|
129
|
+
),
|
|
130
|
+
"WHILE": (
|
|
131
|
+
"**WHILE**\n\n"
|
|
132
|
+
"Repeat the works between `DO` and `ENDWHILE` for as long as the "
|
|
133
|
+
"condition holds true.\n\n"
|
|
134
|
+
"```godcode\nWHILE night < dawn DO\n REVEAL(\"watch\")\nENDWHILE\n```"
|
|
135
|
+
),
|
|
136
|
+
"DO": (
|
|
137
|
+
"**DO**\n\n"
|
|
138
|
+
"Marks the start of the repeated works in a `WHILE` loop.\n\n"
|
|
139
|
+
"```godcode\nWHILE x DO\n REVEAL(x)\nENDWHILE\n```"
|
|
140
|
+
),
|
|
141
|
+
"ENDWHILE": (
|
|
142
|
+
"**ENDWHILE**\n\n"
|
|
143
|
+
"The seal upon a `WHILE` vigil.\n\n```godcode\nENDWHILE\n```"
|
|
144
|
+
),
|
|
145
|
+
"DEFINE": (
|
|
146
|
+
"**DEFINE**\n\n"
|
|
147
|
+
"The first word of a rite's establishment — always paired with "
|
|
148
|
+
"`RITE`. See `DEFINE RITE`.\n\n```godcode\nDEFINE RITE bless()\n"
|
|
149
|
+
"END RITE\n```"
|
|
150
|
+
),
|
|
151
|
+
"RITE": (
|
|
152
|
+
"**RITE**\n\n"
|
|
153
|
+
"A named ceremony of statements. Paired with `DEFINE` to open "
|
|
154
|
+
"and `END` to close. See `DEFINE RITE`.\n\n"
|
|
155
|
+
"```godcode\nDEFINE RITE bless()\nEND RITE\n```"
|
|
156
|
+
),
|
|
157
|
+
"INVOKE": (
|
|
158
|
+
"**INVOKE**\n\n"
|
|
159
|
+
"Call a rite by name, offering it arguments in the ancient manner.\n\n"
|
|
160
|
+
"```godcode\nINVOKE bless(\"the meek\")\n```"
|
|
161
|
+
),
|
|
162
|
+
"RETURN": (
|
|
163
|
+
"**RETURN**\n\n"
|
|
164
|
+
"Offer a value back from a rite to the one who invoked it. "
|
|
165
|
+
"A rite without `RETURN` yields the void.\n\n"
|
|
166
|
+
"```godcode\nRETURN manna * 2\n```"
|
|
167
|
+
),
|
|
168
|
+
"IMPORT": (
|
|
169
|
+
"**IMPORT**\n\n"
|
|
170
|
+
"Bring another scroll into this creation, that its works may "
|
|
171
|
+
"serve here.\n\n```godcode\nIMPORT \"psalms.god\"\n```"
|
|
172
|
+
),
|
|
173
|
+
"REVEAL": (
|
|
174
|
+
"**REVEAL**\n\n"
|
|
175
|
+
"Speak a value aloud — the scroll's voice, printing to the world "
|
|
176
|
+
"beyond.\n\n```godcode\nREVEAL(\"The heavens declare\")\n```"
|
|
177
|
+
),
|
|
178
|
+
"BREATHE": (
|
|
179
|
+
"**BREATHE**\n\n"
|
|
180
|
+
"The first word of awakening — always `BREATHE LIFE INTO name`. "
|
|
181
|
+
"See `BREATHE LIFE INTO`.\n\n"
|
|
182
|
+
"```godcode\nBREATHE LIFE INTO vessel\n```"
|
|
183
|
+
),
|
|
184
|
+
"LIFE": (
|
|
185
|
+
"**LIFE**\n\n"
|
|
186
|
+
"The middle word of `BREATHE LIFE INTO` — the breath itself.\n\n"
|
|
187
|
+
"```godcode\nBREATHE LIFE INTO vessel\n```"
|
|
188
|
+
),
|
|
189
|
+
"INTO": (
|
|
190
|
+
"**INTO**\n\n"
|
|
191
|
+
"The directing word of `BREATHE LIFE INTO name`.\n\n"
|
|
192
|
+
"```godcode\nBREATHE LIFE INTO vessel\n```"
|
|
193
|
+
),
|
|
194
|
+
"PROPHESY": (
|
|
195
|
+
"**PROPHESY**\n\n"
|
|
196
|
+
"Utter a fixed word into the scroll — a literal prophecy of text.\n\n"
|
|
197
|
+
"```godcode\nPROPHESY \"and it was good\"\n```"
|
|
198
|
+
),
|
|
199
|
+
"ASCEND": (
|
|
200
|
+
"**ASCEND**\n\n"
|
|
201
|
+
"End the run in peace. Nothing after `ASCEND` shall come to pass.\n\n"
|
|
202
|
+
"```godcode\nASCEND\n```"
|
|
203
|
+
),
|
|
204
|
+
"SEAL": (
|
|
205
|
+
"**SEAL**\n\n"
|
|
206
|
+
"Set a value under seal — it is recorded in the covenant ledger "
|
|
207
|
+
"and may not be altered thereafter.\n\n```godcode\nSEAL covenant\n```"
|
|
208
|
+
),
|
|
209
|
+
# --- v4.0 ---
|
|
210
|
+
"ANCHOR": (
|
|
211
|
+
"**ANCHOR**\n\n"
|
|
212
|
+
"Anchor a value's hash on a chain: `ANCHOR(expr)` returns a receipt "
|
|
213
|
+
"map `{chain, anchor_hash, height, timestamp, payload_hash}`. "
|
|
214
|
+
"The default chain is `simulated` (a local tamper-evident chain); "
|
|
215
|
+
"a second argument names another registered chain.\n\n"
|
|
216
|
+
"```godcode\nDECLARE seal AS ANCHOR(covenant)\n```"
|
|
217
|
+
),
|
|
218
|
+
"CONSULT": (
|
|
219
|
+
"**CONSULT**\n\n"
|
|
220
|
+
"Ask the local Spirit oracle a question; it answers with two to "
|
|
221
|
+
"three sentences of counsel. No external calls.\n\n"
|
|
222
|
+
"```godcode\nREVEAL(CONSULT(\"How should I structure this covenant?\"))\n```"
|
|
223
|
+
),
|
|
224
|
+
"INTENT": (
|
|
225
|
+
"**INTENT**\n\n"
|
|
226
|
+
"The naming word of `DECLARE INTENT \"words...\" ON rite_name` — "
|
|
227
|
+
"it registers a natural-language intent on a rite. When the rite "
|
|
228
|
+
"is invoked, the Spirit discerns whether its words still walk in "
|
|
229
|
+
"the declared intent, and counsels gently on drift.\n\n"
|
|
230
|
+
"```godcode\nDECLARE INTENT \"bring peace\" ON evening_blessing\n```"
|
|
231
|
+
),
|
|
232
|
+
# --- end v4.0 ---
|
|
233
|
+
"TESTIFY": (
|
|
234
|
+
"**TESTIFY**\n\n"
|
|
235
|
+
"Bear witness to a value — affirm it before the heavens.\n\n"
|
|
236
|
+
"```godcode\nTESTIFY manna > 0\n```"
|
|
237
|
+
),
|
|
238
|
+
"BLESS": (
|
|
239
|
+
"**BLESS**\n\n"
|
|
240
|
+
"Confer blessing upon a name, marking it favored.\n\n"
|
|
241
|
+
"```godcode\nBLESS the_meek\n```"
|
|
242
|
+
),
|
|
243
|
+
"ANOINT": (
|
|
244
|
+
"**ANOINT**\n\n"
|
|
245
|
+
"Anoint a name for a holy purpose, setting it apart.\n\n"
|
|
246
|
+
"```godcode\nANOINT the_chosen\n```"
|
|
247
|
+
),
|
|
248
|
+
"REFLECT": (
|
|
249
|
+
"**REFLECT**\n\n"
|
|
250
|
+
"Pause and contemplate — a still point in the works.\n\n"
|
|
251
|
+
"```godcode\nREFLECT\n```"
|
|
252
|
+
),
|
|
253
|
+
"AND": (
|
|
254
|
+
"**AND**\n\n"
|
|
255
|
+
"Join two truths; the whole holds only if both hold.\n\n"
|
|
256
|
+
"```godcode\nIF faith AND works THEN\n```"
|
|
257
|
+
),
|
|
258
|
+
"OR": (
|
|
259
|
+
"**OR**\n\n"
|
|
260
|
+
"Offer two truths; the whole holds if either holds.\n\n"
|
|
261
|
+
"```godcode\nIF mercy OR grace THEN\n```"
|
|
262
|
+
),
|
|
263
|
+
"NOT": (
|
|
264
|
+
"**NOT**\n\n"
|
|
265
|
+
"Turn a truth upon its head.\n\n```godcode\nIF NOT fear THEN\n```"
|
|
266
|
+
),
|
|
267
|
+
"TRUE": (
|
|
268
|
+
"**TRUE**\n\n"
|
|
269
|
+
"The eternal yes.\n\n```godcode\nDECLARE amen AS true\n```"
|
|
270
|
+
),
|
|
271
|
+
"FALSE": (
|
|
272
|
+
"**FALSE**\n\n"
|
|
273
|
+
"The eternal no.\n\n```godcode\nDECLARE doubt AS false\n```"
|
|
274
|
+
),
|
|
275
|
+
"VOID": (
|
|
276
|
+
"**VOID**\n\n"
|
|
277
|
+
"The absence of all things — what a rite yields when it returns "
|
|
278
|
+
"nothing.\n\n```godcode\nDECLARE emptiness AS void\n```"
|
|
279
|
+
),
|
|
280
|
+
"IS": (
|
|
281
|
+
"**IS**\n\n"
|
|
282
|
+
"The weighing word — reserved for divine comparisons.\n\n"
|
|
283
|
+
"```godcode\n# reserved\n```"
|
|
284
|
+
),
|
|
285
|
+
"END": (
|
|
286
|
+
"**END**\n\n"
|
|
287
|
+
"The closing word — paired with `CREATION` or `RITE` to seal a "
|
|
288
|
+
"block.\n\n```godcode\nEND CREATION\n```"
|
|
289
|
+
),
|
|
290
|
+
# -- built-ins -------------------------------------------------------
|
|
291
|
+
"LEN": (
|
|
292
|
+
"**LEN**(value)\n\n"
|
|
293
|
+
"Measure the length of a string or a list, and the number shall "
|
|
294
|
+
"be revealed.\n\n```godcode\nREVEAL(LEN(\"firmament\"))\n```"
|
|
295
|
+
),
|
|
296
|
+
"STR": (
|
|
297
|
+
"**STR**(value)\n\n"
|
|
298
|
+
"Turn anything into its spoken form — a string.\n\n"
|
|
299
|
+
"```godcode\nREVEAL(STR(40) + \" days\")\n```"
|
|
300
|
+
),
|
|
301
|
+
"NUM": (
|
|
302
|
+
"**NUM**(value)\n\n"
|
|
303
|
+
"Turn a string into a number, that it may be weighed and counted.\n\n"
|
|
304
|
+
"```godcode\nDECLARE years AS NUM(\"40\")\n```"
|
|
305
|
+
),
|
|
306
|
+
"TYPE": (
|
|
307
|
+
"**TYPE**(value)\n\n"
|
|
308
|
+
"Discern the kind of a thing — its type, named aloud.\n\n"
|
|
309
|
+
"```godcode\nREVEAL(TYPE(manna))\n```"
|
|
310
|
+
),
|
|
311
|
+
"RANDOM": (
|
|
312
|
+
"**RANDOM**(bound)\n\n"
|
|
313
|
+
"Cast lots — draw a whole number from 0 up to (but not including) "
|
|
314
|
+
"`bound`.\n\n```godcode\nDECLARE lot AS RANDOM(12)\n```"
|
|
315
|
+
),
|
|
316
|
+
"RANGE": (
|
|
317
|
+
"**RANGE**(stop) / **RANGE**(start, stop)\n\n"
|
|
318
|
+
"Number the days — produce the sequence of whole numbers from "
|
|
319
|
+
"0 (or `start`) up to `stop`.\n\n```godcode\n"
|
|
320
|
+
"FOR day IN RANGE(7)\n REVEAL(day)\nENDFOR\n```"
|
|
321
|
+
),
|
|
322
|
+
"PUSH": (
|
|
323
|
+
"**PUSH**(list, value)\n\n"
|
|
324
|
+
"Add to the multitude — append `value` to the end of `list`.\n\n"
|
|
325
|
+
"```godcode\nPUSH(tribes, \"Benjamin\")\n```"
|
|
326
|
+
),
|
|
327
|
+
"UPPER": (
|
|
328
|
+
"**UPPER**(text)\n\n"
|
|
329
|
+
"Lift every letter to the heavens — uppercase the string.\n\n"
|
|
330
|
+
"```godcode\nREVEAL(UPPER(\"hosanna\"))\n```"
|
|
331
|
+
),
|
|
332
|
+
"LOWER": (
|
|
333
|
+
"**LOWER**(text)\n\n"
|
|
334
|
+
"Humble every letter — lowercase the string.\n\n"
|
|
335
|
+
"```godcode\nREVEAL(LOWER(\"HOSANNA\"))\n```"
|
|
336
|
+
),
|
|
337
|
+
"SPLIT": (
|
|
338
|
+
"**SPLIT**(text, separator)\n\n"
|
|
339
|
+
"Divide the word — split `text` into a list at each `separator`.\n\n"
|
|
340
|
+
"```godcode\nDECLARE words AS SPLIT(\"loaves fishes\", \" \")\n```"
|
|
341
|
+
),
|
|
342
|
+
"JOIN": (
|
|
343
|
+
"**JOIN**(list, separator)\n\n"
|
|
344
|
+
"Gather the scattered — join a list of strings with `separator`.\n\n"
|
|
345
|
+
"```godcode\nREVEAL(JOIN(words, \" \"))\n```"
|
|
346
|
+
),
|
|
347
|
+
"ASK": (
|
|
348
|
+
"**ASK**([prompt])\n\n"
|
|
349
|
+
"Seek counsel — read a line from the one who runs the scroll.\n\n"
|
|
350
|
+
"```godcode\nDECLARE name AS ASK(\"What is your name? \")\n```"
|
|
351
|
+
),
|
|
352
|
+
"BEHOLD": (
|
|
353
|
+
"**BEHOLD**()\n\n"
|
|
354
|
+
"Mark the present hour — the current time, in the heavens' own "
|
|
355
|
+
"notation.\n\n```godcode\nREVEAL(BEHOLD())\n```"
|
|
356
|
+
),
|
|
357
|
+
"REVERSE": (
|
|
358
|
+
"**REVERSE**(value)\n\n"
|
|
359
|
+
"Turn it back upon itself — reverse a string or a list.\n\n"
|
|
360
|
+
"```godcode\nREVEAL(REVERSE(\"stressed\"))\n```"
|
|
361
|
+
),
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# Phrases matched as whole units on hover (case-insensitive).
|
|
365
|
+
_PHRASES = ("BEGIN CREATION", "END CREATION", "DEFINE RITE", "END RITE",
|
|
366
|
+
"BREATHE LIFE INTO")
|
|
367
|
+
|
|
368
|
+
# Snippet-style completions: (label, insertText, detail).
|
|
369
|
+
_SNIPPETS: tuple[tuple[str, str, str], ...] = (
|
|
370
|
+
("BEGIN CREATION … END CREATION",
|
|
371
|
+
"BEGIN CREATION\n\t$0\nEND CREATION",
|
|
372
|
+
"Open a new scroll"),
|
|
373
|
+
("DECLARE … AS …", "DECLARE ${1:name} AS ${2:value}$0",
|
|
374
|
+
"Declare a vessel"),
|
|
375
|
+
("IF … THEN … ENDIF",
|
|
376
|
+
"IF ${1:condition} THEN\n\t$0\nENDIF",
|
|
377
|
+
"Weigh a condition"),
|
|
378
|
+
("FOR … IN … ENDFOR",
|
|
379
|
+
"FOR ${1:item} IN ${2:list}\n\t$0\nENDFOR",
|
|
380
|
+
"Walk a multitude"),
|
|
381
|
+
("WHILE … DO … ENDWHILE",
|
|
382
|
+
"WHILE ${1:condition} DO\n\t$0\nENDWHILE",
|
|
383
|
+
"Keep a vigil"),
|
|
384
|
+
("DEFINE RITE … END RITE",
|
|
385
|
+
"DEFINE RITE ${1:name}(${2:params})\n\t$0\nEND RITE",
|
|
386
|
+
"Establish a rite"),
|
|
387
|
+
("INVOKE …", "INVOKE ${1:name}(${2:args})$0", "Call a rite"),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
_WORD_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
# Diagnostics — the real parser, same as `godcode check`
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
def check_source(text: str) -> list[dict]:
|
|
398
|
+
"""Parse *text* and return LSP diagnostics (empty when pure)."""
|
|
399
|
+
from godcode.errors import GodCodeError
|
|
400
|
+
from godcode.lexer import Lexer
|
|
401
|
+
from godcode.parser import Parser
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
Parser(Lexer(text).lex()).parse()
|
|
405
|
+
except GodCodeError as err:
|
|
406
|
+
line = max((err.line or 1) - 1, 0) # LSP lines are 0-based
|
|
407
|
+
lines = text.splitlines()
|
|
408
|
+
line_len = len(lines[line]) if line < len(lines) else 0
|
|
409
|
+
start_char = max((err.col or 1) - 1, 0)
|
|
410
|
+
start_char = min(start_char, max(line_len - 1, 0))
|
|
411
|
+
end_char = min(start_char + 1, line_len)
|
|
412
|
+
return [{
|
|
413
|
+
"range": {
|
|
414
|
+
"start": {"line": line, "character": start_char},
|
|
415
|
+
"end": {"line": line, "character": end_char},
|
|
416
|
+
},
|
|
417
|
+
"severity": 1, # Error
|
|
418
|
+
"source": "godcode",
|
|
419
|
+
"message": str(err),
|
|
420
|
+
}]
|
|
421
|
+
return []
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# Hover
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
def _phrase_at(line_text: str, char: int) -> str | None:
|
|
429
|
+
upper = line_text.upper()
|
|
430
|
+
for phrase in _PHRASES:
|
|
431
|
+
start = 0
|
|
432
|
+
while True:
|
|
433
|
+
idx = upper.find(phrase, start)
|
|
434
|
+
if idx < 0:
|
|
435
|
+
break
|
|
436
|
+
if idx <= char <= idx + len(phrase):
|
|
437
|
+
return phrase
|
|
438
|
+
start = idx + 1
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def hover_word(line_text: str, char: int) -> str | None:
|
|
443
|
+
"""Return the doc-table key under the cursor, or None."""
|
|
444
|
+
phrase = _phrase_at(line_text, char)
|
|
445
|
+
if phrase is not None:
|
|
446
|
+
return phrase
|
|
447
|
+
for match in _WORD_RE.finditer(line_text):
|
|
448
|
+
if match.start() <= char <= match.end():
|
|
449
|
+
return match.group(0).upper()
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def hover_markdown(word: str | None) -> str | None:
|
|
454
|
+
if word is None:
|
|
455
|
+
return None
|
|
456
|
+
return LSP_DOCS.get(word)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# ---------------------------------------------------------------------------
|
|
460
|
+
# Completion
|
|
461
|
+
# ---------------------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
_COMPLETION_KEYWORDS = sorted(LSP_DOCS)
|
|
464
|
+
_BUILTINS = ("LEN", "STR", "NUM", "TYPE", "RANDOM", "RANGE", "PUSH",
|
|
465
|
+
"UPPER", "LOWER", "SPLIT", "JOIN", "ASK", "BEHOLD", "REVERSE",
|
|
466
|
+
"ANCHOR", "CONSULT")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def completion_items() -> list[dict]:
|
|
470
|
+
items: list[dict] = []
|
|
471
|
+
for kw in _COMPLETION_KEYWORDS:
|
|
472
|
+
items.append({
|
|
473
|
+
"label": kw,
|
|
474
|
+
"kind": 14, # Keyword
|
|
475
|
+
"detail": "God Code " + ("built-in" if kw in _BUILTINS
|
|
476
|
+
else "keyword"),
|
|
477
|
+
"insertText": kw,
|
|
478
|
+
})
|
|
479
|
+
for label, insert, detail in _SNIPPETS:
|
|
480
|
+
items.append({
|
|
481
|
+
"label": label,
|
|
482
|
+
"kind": 15, # Snippet
|
|
483
|
+
"detail": detail,
|
|
484
|
+
"insertText": insert,
|
|
485
|
+
"insertTextFormat": 2, # Snippet
|
|
486
|
+
})
|
|
487
|
+
return items
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# ---------------------------------------------------------------------------
|
|
491
|
+
# The server
|
|
492
|
+
# ---------------------------------------------------------------------------
|
|
493
|
+
|
|
494
|
+
def _log(message: str) -> None:
|
|
495
|
+
sys.stderr.write(f"[godcode-lsp] {message}\n")
|
|
496
|
+
sys.stderr.flush()
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class LanguageServer:
|
|
500
|
+
"""Hand-rolled LSP server over stdio."""
|
|
501
|
+
|
|
502
|
+
def __init__(self,
|
|
503
|
+
stdin: "io.BufferedReader | None" = None,
|
|
504
|
+
stdout: "io.BufferedWriter | None" = None) -> None:
|
|
505
|
+
import io
|
|
506
|
+
self.stdin = stdin or sys.stdin.buffer
|
|
507
|
+
self.stdout = stdout or sys.stdout.buffer
|
|
508
|
+
self.documents: dict[str, str] = {}
|
|
509
|
+
self._shutdown = False
|
|
510
|
+
self._handlers = {
|
|
511
|
+
"initialize": self._on_initialize,
|
|
512
|
+
"initialized": self._on_initialized,
|
|
513
|
+
"shutdown": self._on_shutdown,
|
|
514
|
+
"exit": self._on_exit,
|
|
515
|
+
"textDocument/didOpen": self._on_did_open,
|
|
516
|
+
"textDocument/didChange": self._on_did_change,
|
|
517
|
+
"textDocument/hover": self._on_hover,
|
|
518
|
+
"textDocument/completion": self._on_completion,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# -- transport --------------------------------------------------------
|
|
522
|
+
def _read_message(self) -> dict | None:
|
|
523
|
+
"""Read one Content-Length framed message. None on clean EOF."""
|
|
524
|
+
headers: dict[str, str] = {}
|
|
525
|
+
while True:
|
|
526
|
+
line = self.stdin.readline()
|
|
527
|
+
if not line:
|
|
528
|
+
return None # EOF
|
|
529
|
+
line = line.decode("latin-1").strip()
|
|
530
|
+
if not line:
|
|
531
|
+
break
|
|
532
|
+
if ":" in line:
|
|
533
|
+
name, _, value = line.partition(":")
|
|
534
|
+
headers[name.strip().lower()] = value.strip()
|
|
535
|
+
try:
|
|
536
|
+
length = int(headers.get("content-length", ""))
|
|
537
|
+
except (TypeError, ValueError):
|
|
538
|
+
_log("malformed frame: bad Content-Length; skipping")
|
|
539
|
+
return {}
|
|
540
|
+
try:
|
|
541
|
+
body = self._read_exact(length)
|
|
542
|
+
except EOFError:
|
|
543
|
+
_log("malformed frame: truncated body; skipping")
|
|
544
|
+
return {}
|
|
545
|
+
try:
|
|
546
|
+
return json.loads(body.decode("utf-8"))
|
|
547
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
548
|
+
_log(f"malformed frame: {exc}; skipping")
|
|
549
|
+
return {}
|
|
550
|
+
|
|
551
|
+
def _read_exact(self, length: int) -> bytes:
|
|
552
|
+
body = b""
|
|
553
|
+
while len(body) < length:
|
|
554
|
+
chunk = self.stdin.read(length - len(body))
|
|
555
|
+
if not chunk:
|
|
556
|
+
raise EOFError("truncated frame")
|
|
557
|
+
body += chunk
|
|
558
|
+
return body
|
|
559
|
+
|
|
560
|
+
def _write(self, payload: dict) -> None:
|
|
561
|
+
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
562
|
+
self.stdout.write(
|
|
563
|
+
f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body)
|
|
564
|
+
self.stdout.flush()
|
|
565
|
+
|
|
566
|
+
def _notify(self, method: str, params: dict) -> None:
|
|
567
|
+
self._write({"jsonrpc": "2.0", "method": method, "params": params})
|
|
568
|
+
|
|
569
|
+
def _respond(self, msg_id, result) -> None:
|
|
570
|
+
self._write({"jsonrpc": "2.0", "id": msg_id, "result": result})
|
|
571
|
+
|
|
572
|
+
def _error(self, msg_id, code: int, message: str) -> None:
|
|
573
|
+
self._write({"jsonrpc": "2.0", "id": msg_id,
|
|
574
|
+
"error": {"code": code, "message": message}})
|
|
575
|
+
|
|
576
|
+
# -- main loop ----------------------------------------------------------
|
|
577
|
+
def serve(self) -> int:
|
|
578
|
+
_log("the sanctuary opens (stdio)")
|
|
579
|
+
while True:
|
|
580
|
+
message = self._read_message()
|
|
581
|
+
if message is None:
|
|
582
|
+
_log("stdin closed; ascending")
|
|
583
|
+
return 1 if not self._shutdown else 0
|
|
584
|
+
if not message:
|
|
585
|
+
continue # malformed frame: already logged, keep serving
|
|
586
|
+
method = message.get("method")
|
|
587
|
+
handler = self._handlers.get(method) if method else None
|
|
588
|
+
msg_id = message.get("id")
|
|
589
|
+
if handler is None:
|
|
590
|
+
_log(f"unknown method: {method!r}")
|
|
591
|
+
if msg_id is not None:
|
|
592
|
+
self._error(msg_id, -32601,
|
|
593
|
+
f"Method not found: {method}")
|
|
594
|
+
continue
|
|
595
|
+
try:
|
|
596
|
+
stop = handler(msg_id, message.get("params") or {})
|
|
597
|
+
except Exception as exc: # never let a handler kill the server
|
|
598
|
+
_log(f"handler for {method} failed: {exc}")
|
|
599
|
+
if msg_id is not None:
|
|
600
|
+
self._error(msg_id, -32603,
|
|
601
|
+
f"Internal error: {exc}")
|
|
602
|
+
continue
|
|
603
|
+
if stop:
|
|
604
|
+
return 0
|
|
605
|
+
# pragma: no cover - unreachable
|
|
606
|
+
|
|
607
|
+
# -- handlers -------------------------------------------------------------
|
|
608
|
+
def _on_initialize(self, msg_id, params: dict) -> None: # noqa: ARG002
|
|
609
|
+
_log("initialize received")
|
|
610
|
+
self._respond(msg_id, {
|
|
611
|
+
"capabilities": {
|
|
612
|
+
"textDocumentSync": 1, # Full
|
|
613
|
+
"hoverProvider": True,
|
|
614
|
+
"completionProvider": {"triggerCharacters": []},
|
|
615
|
+
},
|
|
616
|
+
"serverInfo": {"name": "godcode-lsp", "version": "4.0.0"},
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
def _on_initialized(self, msg_id, params: dict) -> None: # noqa: ARG002
|
|
620
|
+
_log("initialized; no-op")
|
|
621
|
+
|
|
622
|
+
def _on_shutdown(self, msg_id, params: dict) -> None: # noqa: ARG002
|
|
623
|
+
_log("shutdown requested")
|
|
624
|
+
self._shutdown = True
|
|
625
|
+
if msg_id is not None:
|
|
626
|
+
self._respond(msg_id, None)
|
|
627
|
+
|
|
628
|
+
def _on_exit(self, msg_id, params: dict) -> bool: # noqa: ARG002
|
|
629
|
+
_log("exit; the sanctuary rests")
|
|
630
|
+
return True
|
|
631
|
+
|
|
632
|
+
def _on_did_open(self, msg_id, params: dict) -> None: # noqa: ARG002
|
|
633
|
+
doc = params.get("textDocument", {})
|
|
634
|
+
uri = doc.get("uri", "")
|
|
635
|
+
text = doc.get("text", "")
|
|
636
|
+
self.documents[uri] = text
|
|
637
|
+
self._publish_diagnostics(uri, text)
|
|
638
|
+
|
|
639
|
+
def _on_did_change(self, msg_id, params: dict) -> None: # noqa: ARG002
|
|
640
|
+
doc = params.get("textDocument", {})
|
|
641
|
+
uri = doc.get("uri", "")
|
|
642
|
+
text = self.documents.get(uri, "")
|
|
643
|
+
for change in params.get("contentChanges", []):
|
|
644
|
+
if "range" not in change: # full-document sync
|
|
645
|
+
text = change.get("text", "")
|
|
646
|
+
self.documents[uri] = text
|
|
647
|
+
self._publish_diagnostics(uri, text)
|
|
648
|
+
|
|
649
|
+
def _publish_diagnostics(self, uri: str, text: str) -> None:
|
|
650
|
+
diagnostics = check_source(text)
|
|
651
|
+
_log(f"diagnostics for {uri}: {len(diagnostics)} error(s)")
|
|
652
|
+
self._notify("textDocument/publishDiagnostics",
|
|
653
|
+
{"uri": uri, "diagnostics": diagnostics})
|
|
654
|
+
|
|
655
|
+
def _on_hover(self, msg_id, params: dict) -> None:
|
|
656
|
+
doc = params.get("textDocument", {})
|
|
657
|
+
uri = doc.get("uri", "")
|
|
658
|
+
pos = params.get("position", {})
|
|
659
|
+
line_no = pos.get("line", 0)
|
|
660
|
+
char = pos.get("character", 0)
|
|
661
|
+
text = self.documents.get(uri, "")
|
|
662
|
+
lines = text.splitlines()
|
|
663
|
+
line_text = lines[line_no] if 0 <= line_no < len(lines) else ""
|
|
664
|
+
word = hover_word(line_text, char)
|
|
665
|
+
markdown = hover_markdown(word)
|
|
666
|
+
_log(f"hover at {uri}:{line_no}:{char} -> {word!r}")
|
|
667
|
+
self._respond(msg_id, {"contents": {"kind": "markdown",
|
|
668
|
+
"value": markdown}}
|
|
669
|
+
if markdown else None)
|
|
670
|
+
|
|
671
|
+
def _on_completion(self, msg_id, params: dict) -> None: # noqa: ARG002
|
|
672
|
+
self._respond(msg_id, completion_items())
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def serve() -> int:
|
|
676
|
+
"""Entry point for ``godcode lsp``."""
|
|
677
|
+
return LanguageServer().serve()
|