@lemmabase/lemma-engine 0.8.11

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.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @lemmabase/lemma-engine — public entry.
3
+ * Glue: lemma.bindings.js (wasm-pack output, renamed by build).
4
+ */
5
+ import initWasm, * as bindings from './lemma.bindings.js';
6
+
7
+ let wasmReady = null;
8
+
9
+ export async function init() {
10
+ if (wasmReady === null) wasmReady = initWasm();
11
+ return wasmReady;
12
+ }
13
+
14
+ export function initSync(module) {
15
+ return bindings.initSync(module);
16
+ }
17
+
18
+ export async function Lemma() {
19
+ await init();
20
+ return new bindings.Engine();
21
+ }
22
+
23
+ export const Engine = bindings.Engine;
package/lemma_bg.wasm ADDED
Binary file
@@ -0,0 +1,36 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ export const memory: WebAssembly.Memory;
4
+ export const __wbg_serverconfig_free: (a: number, b: number) => void;
5
+ export const serve: (a: number) => number;
6
+ export const serverconfig_new: (a: number, b: number) => number;
7
+ export const __wbg_intounderlyingbytesource_free: (a: number, b: number) => void;
8
+ export const __wbg_intounderlyingsink_free: (a: number, b: number) => void;
9
+ export const __wbg_intounderlyingsource_free: (a: number, b: number) => void;
10
+ export const intounderlyingbytesource_autoAllocateChunkSize: (a: number) => number;
11
+ export const intounderlyingbytesource_cancel: (a: number) => void;
12
+ export const intounderlyingbytesource_pull: (a: number, b: number) => number;
13
+ export const intounderlyingbytesource_start: (a: number, b: number) => void;
14
+ export const intounderlyingbytesource_type: (a: number) => number;
15
+ export const intounderlyingsink_abort: (a: number, b: number) => number;
16
+ export const intounderlyingsink_close: (a: number) => number;
17
+ export const intounderlyingsink_write: (a: number, b: number) => number;
18
+ export const intounderlyingsource_cancel: (a: number) => void;
19
+ export const intounderlyingsource_pull: (a: number, b: number) => number;
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;
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;
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;
25
+ export const wasmengine_new: () => number;
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_10702: (a: number, b: number, c: number, d: number) => void;
29
+ export const __wasm_bindgen_func_elem_2761: (a: number, b: number, c: number, d: number) => void;
30
+ export const __wasm_bindgen_func_elem_10709: (a: number, b: number, c: number, d: number) => void;
31
+ export const __wbindgen_export: (a: number, b: number) => number;
32
+ export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
33
+ export const __wbindgen_export3: (a: number) => void;
34
+ export const __wbindgen_export4: (a: number, b: number, c: number) => void;
35
+ export const __wbindgen_export5: (a: number, b: number) => void;
36
+ export const __wbindgen_add_to_stack_pointer: (a: number) => number;
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 ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@lemmabase/lemma-engine",
3
+ "version": "0.8.11",
4
+ "description": "A language that means business. Also in the browser.",
5
+ "type": "module",
6
+ "main": "lemma.js",
7
+ "types": "lemma.d.ts",
8
+ "files": [
9
+ "lemma_bg.wasm",
10
+ "lemma.bindings.js",
11
+ "lemma.bindings.d.ts",
12
+ "lemma.js",
13
+ "lemma.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"
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
+ },
32
+ "keywords": [
33
+ "lemma",
34
+ "rules-engine",
35
+ "business-rules",
36
+ "policy-engine",
37
+ "declarative",
38
+ "dsl",
39
+ "typed",
40
+ "wasm",
41
+ "webassembly",
42
+ "lsp",
43
+ "language-server",
44
+ "monaco-editor"
45
+ ],
46
+ "author": "Ben Rogmans <ben@amrai.nl>",
47
+ "license": "Apache-2.0",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/lemma/lemma"
51
+ },
52
+ "homepage": "https://github.com/lemma/lemma",
53
+ "bugs": {
54
+ "url": "https://github.com/lemma/lemma/issues"
55
+ }
56
+ }