@benrogmans/lemma-engine 0.8.1 → 0.8.3

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.
package/lemma_bg.wasm CHANGED
Binary file
@@ -7,9 +7,6 @@ export const serverconfig_new: (a: number, b: number) => number;
7
7
  export const __wbg_intounderlyingbytesource_free: (a: number, b: number) => void;
8
8
  export const __wbg_intounderlyingsink_free: (a: number, b: number) => void;
9
9
  export const __wbg_intounderlyingsource_free: (a: number, b: number) => void;
10
- export const __wbg_pipeoptions_free: (a: number, b: number) => void;
11
- export const __wbg_queuingstrategy_free: (a: number, b: number) => void;
12
- export const __wbg_readablestreamgetreaderoptions_free: (a: number, b: number) => void;
13
10
  export const intounderlyingbytesource_autoAllocateChunkSize: (a: number) => number;
14
11
  export const intounderlyingbytesource_cancel: (a: number) => void;
15
12
  export const intounderlyingbytesource_pull: (a: number, b: number) => number;
@@ -20,27 +17,19 @@ export const intounderlyingsink_close: (a: number) => number;
20
17
  export const intounderlyingsink_write: (a: number, b: number) => number;
21
18
  export const intounderlyingsource_cancel: (a: number) => void;
22
19
  export const intounderlyingsource_pull: (a: number, b: number) => number;
23
- export const pipeoptions_preventAbort: (a: number) => number;
24
- export const pipeoptions_preventCancel: (a: number) => number;
25
- export const pipeoptions_preventClose: (a: number) => number;
26
- export const pipeoptions_signal: (a: number) => number;
27
- export const queuingstrategy_highWaterMark: (a: number) => number;
28
- export const readablestreamgetreaderoptions_mode: (a: number) => number;
29
- export const __wbg_wasmengine_free: (a: number, b: number) => void;
30
- export const wasmengine_addLemmaFile: (a: number, b: number, c: number, d: number, e: number) => number;
31
- export const wasmengine_evaluate: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => void;
32
- export const wasmengine_evaluateEffective: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => void;
33
- export const wasmengine_formatSource: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
34
- export const wasmengine_getSchema: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
35
- export const wasmengine_getSchemaEffective: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
20
+ export const __wbg_engine_free: (a: number, b: number) => void;
21
+ export const wasmengine_format: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
36
22
  export const wasmengine_invert: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => void;
37
- export const wasmengine_listSpecs: (a: number, b: number) => void;
23
+ export const wasmengine_list: (a: number, b: number) => void;
24
+ export const wasmengine_load: (a: number, b: number, c: number, d: number, e: number) => number;
38
25
  export const wasmengine_new: () => number;
39
- export const __wasm_bindgen_func_elem_9296: (a: number, b: number) => void;
40
- export const __wasm_bindgen_func_elem_356: (a: number, b: number) => void;
41
- export const __wasm_bindgen_func_elem_10439: (a: number, b: number, c: number, d: number) => void;
42
- export const __wasm_bindgen_func_elem_2709: (a: number, b: number, c: number, d: number) => void;
43
- export const __wasm_bindgen_func_elem_10458: (a: number, b: number, c: number, d: number) => void;
26
+ export const wasmengine_run: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
27
+ export const wasmengine_schema: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
28
+ export const __wasm_bindgen_func_elem_9731: (a: number, b: number) => void;
29
+ export const __wasm_bindgen_func_elem_385: (a: number, b: number) => void;
30
+ export const __wasm_bindgen_func_elem_10982: (a: number, b: number, c: number, d: number) => void;
31
+ export const __wasm_bindgen_func_elem_2764: (a: number, b: number, c: number, d: number) => void;
32
+ export const __wasm_bindgen_func_elem_10985: (a: number, b: number, c: number, d: number) => void;
44
33
  export const __wbindgen_export: (a: number, b: number) => number;
45
34
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
46
35
  export const __wbindgen_export3: (a: number) => void;
package/lsp-client.js ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * LSP client for the Lemma WASM playground.
3
+ *
4
+ * Handles LSP JSON-RPC protocol framing (Content-Length headers),
5
+ * stream plumbing for the WASM LSP server, and Monaco marker updates.
6
+ */
7
+
8
+ import { serve, ServerConfig } from './lsp.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Async channel: a simple queue that backs an AsyncIterator for the server.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Queue-backed channel that implements AsyncIterable (Symbol.asyncIterator)
16
+ * so Rust js_sys::AsyncIterator can consume it. Yields Uint8Array chunks.
17
+ */
18
+ function createChannel() {
19
+ const queue = [];
20
+ let waiting = null;
21
+ let closed = false;
22
+
23
+ const channel = {
24
+ send(data) {
25
+ if (closed) return;
26
+ if (waiting) {
27
+ const resolve = waiting;
28
+ waiting = null;
29
+ resolve({ value: data, done: false });
30
+ } else {
31
+ queue.push(data);
32
+ }
33
+ },
34
+ close() {
35
+ closed = true;
36
+ if (waiting) {
37
+ const resolve = waiting;
38
+ waiting = null;
39
+ resolve({ value: undefined, done: true });
40
+ }
41
+ },
42
+ next() {
43
+ if (queue.length > 0) {
44
+ return Promise.resolve({ value: queue.shift(), done: false });
45
+ }
46
+ if (closed) {
47
+ return Promise.resolve({ value: undefined, done: true });
48
+ }
49
+ return new Promise(function (resolve) { waiting = resolve; });
50
+ },
51
+ };
52
+ channel[Symbol.asyncIterator] = function () { return this; };
53
+ return channel;
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // LSP protocol framing
58
+ // ---------------------------------------------------------------------------
59
+
60
+ const HEADER_SEPARATOR = '\r\n\r\n';
61
+ const CONTENT_LENGTH_RE = /Content-Length:\s*(\d+)/i;
62
+ const encoder = new TextEncoder();
63
+ const decoder = new TextDecoder();
64
+
65
+ function encodeMessage(json) {
66
+ const body = JSON.stringify(json);
67
+ const bodyBytes = encoder.encode(body);
68
+ const header = 'Content-Length: ' + bodyBytes.byteLength + HEADER_SEPARATOR;
69
+ const headerBytes = encoder.encode(header);
70
+ const frame = new Uint8Array(headerBytes.byteLength + bodyBytes.byteLength);
71
+ frame.set(headerBytes, 0);
72
+ frame.set(bodyBytes, headerBytes.byteLength);
73
+ return frame;
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // LSP Client
78
+ // ---------------------------------------------------------------------------
79
+
80
+ export class LspClient {
81
+ /**
82
+ * @param {object} monacoInstance The `monaco` global (for setModelMarkers)
83
+ */
84
+ constructor(monacoInstance) {
85
+ this._monaco = monacoInstance;
86
+ this._nextId = 1;
87
+ this._pending = new Map();
88
+ this._diagnosticsCallback = null;
89
+ this._channel = null;
90
+ this._receiveBuffer = new Uint8Array(0);
91
+ this._running = false;
92
+ }
93
+
94
+ /**
95
+ * Launch the WASM LSP server. Uses bundled serve/ServerConfig unless overridden.
96
+ * @param {Function} [serveFn] Optional; defaults to bundled LSP serve
97
+ * @param {Function} [ServerConfigCls] Optional; defaults to bundled ServerConfig
98
+ */
99
+ async start(serveFn, ServerConfigCls) {
100
+ const runServe = serveFn ?? serve;
101
+ const RunConfig = ServerConfigCls ?? ServerConfig;
102
+
103
+ this._channel = createChannel();
104
+
105
+ const self = this;
106
+
107
+ const serverToClient = new WritableStream({
108
+ write(chunk) {
109
+ if (chunk instanceof Uint8Array) {
110
+ self._onServerBytes(chunk);
111
+ }
112
+ },
113
+ });
114
+
115
+ const config = new RunConfig(this._channel, serverToClient);
116
+ this._running = true;
117
+
118
+ runServe(config).then(function () {
119
+ self._running = false;
120
+ }).catch(function (err) {
121
+ console.error('LSP server stopped:', err);
122
+ self._running = false;
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Send the LSP initialize request and the initialized notification.
128
+ */
129
+ async initialize() {
130
+ await this.sendRequest('initialize', {
131
+ processId: null,
132
+ capabilities: {},
133
+ rootUri: null,
134
+ });
135
+ this.sendNotification('initialized', {});
136
+ }
137
+
138
+ /**
139
+ * Notify the server that a file was opened.
140
+ */
141
+ didOpen(uri, languageId, version, text) {
142
+ this.sendNotification('textDocument/didOpen', {
143
+ textDocument: { uri: uri, languageId: languageId, version: version, text: text },
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Notify the server that a file changed (full sync).
149
+ */
150
+ didChange(uri, version, text) {
151
+ this.sendNotification('textDocument/didChange', {
152
+ textDocument: { uri: uri, version: version },
153
+ contentChanges: [{ text: text }],
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Request textDocument/formatting from the server.
159
+ * Returns an array of TextEdit.
160
+ */
161
+ async formatting(uri, tabSize, insertSpaces) {
162
+ return this.sendRequest('textDocument/formatting', {
163
+ textDocument: { uri: uri },
164
+ options: { tabSize: tabSize, insertSpaces: insertSpaces },
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Request textDocument/semanticTokens/full from the server.
170
+ * Returns { data: number[] } or null.
171
+ */
172
+ async semanticTokensFull(uri) {
173
+ return this.sendRequest('textDocument/semanticTokens/full', {
174
+ textDocument: { uri: uri },
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Register a callback for textDocument/publishDiagnostics notifications.
180
+ * @param {Function} callback Receives (uri, diagnostics)
181
+ */
182
+ onDiagnostics(callback) {
183
+ this._diagnosticsCallback = callback;
184
+ }
185
+
186
+ /**
187
+ * Send a JSON-RPC request and return a Promise for the result.
188
+ */
189
+ sendRequest(method, params) {
190
+ const id = this._nextId++;
191
+ const msg = { jsonrpc: '2.0', id: id, method: method, params: params };
192
+ this._send(msg);
193
+ const pending = this._pending;
194
+ return new Promise(function (resolve, reject) {
195
+ pending.set(id, { resolve: resolve, reject: reject });
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Send a JSON-RPC notification (no response expected).
201
+ */
202
+ sendNotification(method, params) {
203
+ const msg = { jsonrpc: '2.0', method: method, params: params };
204
+ this._send(msg);
205
+ }
206
+
207
+ /**
208
+ * Stop the LSP server by closing the input channel.
209
+ */
210
+ stop() {
211
+ if (this._channel) {
212
+ this._channel.close();
213
+ }
214
+ this._running = false;
215
+ }
216
+
217
+ // -- internals --
218
+
219
+ _send(json) {
220
+ if (!this._channel) return;
221
+ this._channel.send(encodeMessage(json));
222
+ }
223
+
224
+ _onServerBytes(chunk) {
225
+ const merged = new Uint8Array(this._receiveBuffer.byteLength + chunk.byteLength);
226
+ merged.set(this._receiveBuffer, 0);
227
+ merged.set(chunk, this._receiveBuffer.byteLength);
228
+ this._receiveBuffer = merged;
229
+
230
+ this._drainFrames();
231
+ }
232
+
233
+ _drainFrames() {
234
+ while (true) {
235
+ const buf = this._receiveBuffer;
236
+ const text = decoder.decode(buf);
237
+
238
+ const sepIndex = text.indexOf(HEADER_SEPARATOR);
239
+ if (sepIndex === -1) break;
240
+
241
+ const headerPart = text.substring(0, sepIndex);
242
+ const match = CONTENT_LENGTH_RE.exec(headerPart);
243
+ if (!match) {
244
+ console.error('LSP: missing Content-Length in header:', headerPart);
245
+ break;
246
+ }
247
+ const contentLength = parseInt(match[1], 10);
248
+
249
+ const headerByteLength = encoder.encode(headerPart + HEADER_SEPARATOR).byteLength;
250
+ const totalNeeded = headerByteLength + contentLength;
251
+
252
+ if (buf.byteLength < totalNeeded) break;
253
+
254
+ const bodyBytes = buf.slice(headerByteLength, totalNeeded);
255
+ this._receiveBuffer = buf.slice(totalNeeded);
256
+
257
+ const bodyText = decoder.decode(bodyBytes);
258
+ let json;
259
+ try {
260
+ json = JSON.parse(bodyText);
261
+ } catch (e) {
262
+ console.error('LSP: failed to parse JSON body:', bodyText, e);
263
+ continue;
264
+ }
265
+
266
+ this._handleMessage(json);
267
+ }
268
+ }
269
+
270
+ _handleMessage(msg) {
271
+ if (msg.id !== undefined && msg.id !== null && this._pending.has(msg.id)) {
272
+ const entry = this._pending.get(msg.id);
273
+ this._pending.delete(msg.id);
274
+ if (msg.error) {
275
+ entry.reject(msg.error);
276
+ } else {
277
+ entry.resolve(msg.result);
278
+ }
279
+ return;
280
+ }
281
+
282
+ if (msg.method === 'textDocument/publishDiagnostics' && msg.params) {
283
+ if (this._diagnosticsCallback) {
284
+ this._diagnosticsCallback(msg.params.uri, msg.params.diagnostics || []);
285
+ }
286
+ return;
287
+ }
288
+ }
289
+
290
+ // -- Monaco integration --
291
+
292
+ /**
293
+ * Convert LSP diagnostics to Monaco markers and set them on the given model.
294
+ *
295
+ * @param {object} model Monaco editor model
296
+ * @param {Array} diagnostics LSP Diagnostic[]
297
+ */
298
+ setMonacoMarkers(model, diagnostics) {
299
+ const monaco = this._monaco;
300
+ if (!monaco) return;
301
+
302
+ const markers = diagnostics.map(function (d) {
303
+ return {
304
+ startLineNumber: d.range.start.line + 1,
305
+ startColumn: d.range.start.character + 1,
306
+ endLineNumber: d.range.end.line + 1,
307
+ endColumn: d.range.end.character + 1,
308
+ message: d.message,
309
+ severity: lspSeverityToMonaco(monaco, d.severity),
310
+ };
311
+ });
312
+
313
+ monaco.editor.setModelMarkers(model, 'lemma-lsp', markers);
314
+ }
315
+ }
316
+
317
+ function lspSeverityToMonaco(monaco, severity) {
318
+ switch (severity) {
319
+ case 1: return monaco.MarkerSeverity.Error;
320
+ case 2: return monaco.MarkerSeverity.Warning;
321
+ case 3: return monaco.MarkerSeverity.Info;
322
+ case 4: return monaco.MarkerSeverity.Hint;
323
+ default: return monaco.MarkerSeverity.Error;
324
+ }
325
+ }
package/lsp.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { serve, ServerConfig } from './lemma.bindings.js';
package/lsp.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Browser LSP (streams). Call init() from package root first.
3
+ */
4
+ export { serve, ServerConfig } from './lemma.bindings.js';
package/monaco.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Monaco Editor language registration for Lemma.
3
+ * Syntax highlighting is provided by the Rust LSP via semantic tokens.
4
+ */
5
+
6
+ export const SEMANTIC_TOKEN_TYPES = [
7
+ 'keyword',
8
+ 'type',
9
+ 'function',
10
+ 'variable',
11
+ 'number',
12
+ 'string',
13
+ 'comment',
14
+ 'operator',
15
+ 'enumMember',
16
+ 'property',
17
+ ];
18
+
19
+ export function registerLemmaLanguage(monaco) {
20
+ monaco.languages.register({ id: 'lemma' });
21
+
22
+ monaco.languages.setLanguageConfiguration('lemma', {
23
+ comments: { blockComment: ['"""', '"""'] },
24
+ brackets: [['(', ')'], ['[', ']']],
25
+ autoClosingPairs: [
26
+ { open: '(', close: ')' },
27
+ { open: '[', close: ']' },
28
+ { open: '"', close: '"' },
29
+ ],
30
+ surroundingPairs: [
31
+ { open: '(', close: ')' },
32
+ { open: '[', close: ']' },
33
+ { open: '"', close: '"' },
34
+ ],
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Register LSP-backed providers for semantic tokens and formatting.
40
+ * Call after the LspClient has been initialized and didOpen sent.
41
+ * URI for each request is derived from model.uri (multi-file support).
42
+ *
43
+ * @param {object} monaco The monaco-editor API object
44
+ * @param {object} lspClient An initialized LspClient instance
45
+ */
46
+ export function registerLspProviders(monaco, lspClient) {
47
+ monaco.languages.registerDocumentSemanticTokensProvider('lemma', {
48
+ getLegend() {
49
+ return { tokenTypes: SEMANTIC_TOKEN_TYPES, tokenModifiers: [] };
50
+ },
51
+ async provideDocumentSemanticTokens(model) {
52
+ const uri = model.uri.toString();
53
+ const result = await lspClient.semanticTokensFull(uri);
54
+ if (!result || !result.data) return null;
55
+ return { data: new Uint32Array(result.data) };
56
+ },
57
+ releaseDocumentSemanticTokens() {},
58
+ });
59
+
60
+ monaco.languages.registerDocumentFormattingEditProvider('lemma', {
61
+ async provideDocumentFormattingEdits(model) {
62
+ const uri = model.uri.toString();
63
+ const edits = await lspClient.formatting(
64
+ uri,
65
+ model.getOptions().tabSize,
66
+ model.getOptions().insertSpaces,
67
+ );
68
+ if (!Array.isArray(edits)) return [];
69
+ return edits.map(function (edit) {
70
+ return {
71
+ range: new monaco.Range(
72
+ edit.range.start.line + 1, edit.range.start.character + 1,
73
+ edit.range.end.line + 1, edit.range.end.character + 1,
74
+ ),
75
+ text: edit.newText,
76
+ };
77
+ });
78
+ },
79
+ });
80
+ }
package/package.json CHANGED
@@ -1,22 +1,49 @@
1
1
  {
2
2
  "name": "@benrogmans/lemma-engine",
3
- "version": "0.8.1",
4
- "description": "Language Server Protocol implementation for Lemma",
3
+ "version": "0.8.3",
4
+ "description": "A language that means business. Also in the browser.",
5
5
  "type": "module",
6
6
  "main": "lemma.js",
7
7
  "types": "lemma.d.ts",
8
8
  "files": [
9
9
  "lemma_bg.wasm",
10
+ "lemma.bindings.js",
11
+ "lemma.bindings.d.ts",
10
12
  "lemma.js",
11
13
  "lemma.d.ts",
12
- "lemma_bg.js",
13
- "lemma_bg.wasm.d.ts"
14
+ "lsp.js",
15
+ "lsp.d.ts",
16
+ "lemma_bg.wasm.d.ts",
17
+ "lsp-client.js",
18
+ "monaco.js",
19
+ "esbuild.js",
20
+ "lemma.iife.js",
21
+ "README.md",
22
+ "LICENSE"
14
23
  ],
24
+ "exports": {
25
+ ".": "./lemma.js",
26
+ "./lsp": "./lsp.js",
27
+ "./lsp-client": "./lsp-client.js",
28
+ "./monaco": "./monaco.js",
29
+ "./iife": "./lemma.iife.js",
30
+ "./esbuild": "./esbuild.js"
31
+ },
15
32
  "keywords": [
33
+ "lemma",
34
+ "rules-engine",
35
+ "business-rules",
36
+ "policy-engine",
37
+ "declarative",
38
+ "dsl",
39
+ "typed",
16
40
  "wasm",
17
- "webassembly"
41
+ "webassembly",
42
+ "lsp",
43
+ "language-server",
44
+ "monaco-editor"
18
45
  ],
19
- "author": "benrogmans <ben@amrai.nl>",
46
+ "author": "Ben Rogmans <ben@amrai.nl>",
20
47
  "license": "Apache-2.0",
21
48
  "repository": {
22
49
  "type": "git",