@lango-project/lango-js 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,67 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+
3
+ export function createSqliteAdapter(filename = ":memory:") {
4
+ const database = new DatabaseSync(filename);
5
+ let depth = 0;
6
+ let state = null;
7
+
8
+ return {
9
+ execute(sql, parameters = {}) {
10
+ const statement = database.prepare(sql);
11
+ if (/^\s*(select|pragma|with)\b/i.test(sql) || /\breturning\b/i.test(sql))
12
+ return { rows: statement.all(parameters).map((row) => ({ ...row })), changes: null, lastInsertRowid: null };
13
+ const result = statement.run(parameters);
14
+ const dataChange = /^\s*(insert|update|delete)\b/i.test(sql);
15
+ const valuesAt = sql.search(/\bvalues\s*\(/i);
16
+ const singleRowInsert =
17
+ /^\s*insert\b/i.test(sql) &&
18
+ valuesAt >= 0 &&
19
+ !/\)\s*,\s*\(/.test(sql.slice(valuesAt));
20
+ return {
21
+ rows: [],
22
+ changes: dataChange ? Number(result.changes) : null,
23
+ lastInsertRowid: singleRowInsert && result.lastInsertRowid !== undefined
24
+ ? Number(result.lastInsertRowid)
25
+ : null,
26
+ };
27
+ },
28
+ begin() {
29
+ if (depth === 0) database.exec("BEGIN");
30
+ if (depth === 0) state = "active";
31
+ depth += 1;
32
+ },
33
+ commit() {
34
+ if (depth === 0) throw new Error("NoActiveTransaction");
35
+ depth -= 1;
36
+ if (depth === 0) {
37
+ if (state === "rollback_only") {
38
+ database.exec("ROLLBACK");
39
+ state = null;
40
+ throw new Error("TransactionRolledBack");
41
+ }
42
+ database.exec("COMMIT");
43
+ state = null;
44
+ }
45
+ },
46
+ rollback() {
47
+ if (depth === 0) throw new Error("NoActiveTransaction");
48
+ if (depth > 1) {
49
+ depth -= 1;
50
+ state = "rollback_only";
51
+ } else {
52
+ depth = 0;
53
+ database.exec("ROLLBACK");
54
+ state = null;
55
+ }
56
+ },
57
+ transactionState() { return state; },
58
+ close() {
59
+ if (depth > 0) {
60
+ depth = 0;
61
+ state = null;
62
+ database.exec("ROLLBACK");
63
+ }
64
+ database.close();
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,487 @@
1
+ export const Decimal = Object.freeze({
2
+ Rounding: Object.freeze({
3
+ Up: "up", Down: "down", Ceil: "ceil", Floor: "floor",
4
+ HalfUp: "half_up", HalfDown: "half_down", HalfEven: "half_even",
5
+ HalfCeil: "half_ceil", HalfFloor: "half_floor",
6
+ }),
7
+ });
8
+
9
+ const ROUNDING_MODES = Object.freeze({
10
+ [Decimal.Rounding.Up]: "ROUND_UP", [Decimal.Rounding.Down]: "ROUND_DOWN",
11
+ [Decimal.Rounding.Ceil]: "ROUND_CEIL", [Decimal.Rounding.Floor]: "ROUND_FLOOR",
12
+ [Decimal.Rounding.HalfUp]: "ROUND_HALF_UP", [Decimal.Rounding.HalfDown]: "ROUND_HALF_DOWN",
13
+ [Decimal.Rounding.HalfEven]: "ROUND_HALF_EVEN", [Decimal.Rounding.HalfCeil]: "ROUND_HALF_CEIL",
14
+ [Decimal.Rounding.HalfFloor]: "ROUND_HALF_FLOOR",
15
+ });
16
+
17
+ export function runtimeSource(profile = {}) {
18
+ const scale = profile.decimal_division_scale ?? 6;
19
+ const rounding = profile.decimal_division_rounding ?? Decimal.Rounding.HalfEven;
20
+ if (!Number.isInteger(scale) || scale < 0)
21
+ throw new Error("decimal_division_scale must be a non-negative integer");
22
+ if (!(rounding in ROUNDING_MODES))
23
+ throw new Error(`unknown decimal_division_rounding ${rounding}`);
24
+ return `import BigNumber from "./bignumber.mjs";
25
+
26
+ const Decimal = BigNumber.clone({
27
+ DECIMAL_PLACES: ${scale},
28
+ ROUNDING_MODE: BigNumber.${ROUNDING_MODES[rounding]},
29
+ });
30
+ Decimal.Rounding = Object.freeze(${JSON.stringify(Decimal.Rounding)});
31
+ export { Decimal };
32
+
33
+ export function decimal(value) {
34
+ return new Decimal(value);
35
+ }
36
+
37
+ export function isDecimal(value) {
38
+ return value instanceof Decimal;
39
+ }
40
+
41
+ export function toJsNumber(value) {
42
+ return new Decimal(value).toNumber();
43
+ }
44
+
45
+ let catalog = {};
46
+ let output = null;
47
+ let database = null;
48
+ let failedTransaction = false;
49
+ export const DB = Object.freeze({ Active: "active", RollbackOnly: "rollback_only", Failed: "failed" });
50
+ const contexts = new Map();
51
+
52
+ export class RuntimeConfigurationError extends Error {
53
+ constructor(message) {
54
+ super(message);
55
+ this.name = "RuntimeConfigurationError";
56
+ }
57
+ }
58
+
59
+ export function configure(options = {}) {
60
+ if (options === null || typeof options !== "object" || Array.isArray(options))
61
+ throw new RuntimeConfigurationError("runtime configuration must be an object");
62
+ const nextCatalog = options.catalog ?? {};
63
+ const nextOutput = options.output ?? null;
64
+ const nextDatabase = options.database_adapter ?? null;
65
+ if (nextCatalog === null || typeof nextCatalog !== "object" || Array.isArray(nextCatalog))
66
+ throw new RuntimeConfigurationError("runtime catalog must be an object");
67
+ if (nextOutput !== null && typeof nextOutput !== "function")
68
+ throw new RuntimeConfigurationError("runtime output must be a function or null");
69
+ if (nextDatabase !== null && (typeof nextDatabase !== "object" && typeof nextDatabase !== "function"))
70
+ throw new RuntimeConfigurationError("runtime database_adapter must be an object or null");
71
+ catalog = nextCatalog;
72
+ output = nextOutput;
73
+ database = nextDatabase;
74
+ failedTransaction = false;
75
+ contexts.clear();
76
+ return null;
77
+ }
78
+
79
+ function requireDatabase() {
80
+ if (database === null) throw new Error("DatabaseNotConfigured");
81
+ return database;
82
+ }
83
+
84
+ export function startTrans() {
85
+ if (failedTransaction) throw new Error("TransactionFailed");
86
+ requireDatabase().begin();
87
+ return null;
88
+ }
89
+ export function commitTrans() {
90
+ if (failedTransaction) throw new Error("TransactionFailed");
91
+ requireDatabase().commit();
92
+ return null;
93
+ }
94
+ export function rollbackTrans() {
95
+ const adapter = requireDatabase();
96
+ adapter.rollback();
97
+ if (adapter.transactionState() === null) failedTransaction = false;
98
+ return null;
99
+ }
100
+ export function transState() {
101
+ const state = requireDatabase().transactionState();
102
+ return failedTransaction && state !== null ? DB.Failed : state;
103
+ }
104
+
105
+ export class SqlError extends Error {
106
+ constructor(cause, query) {
107
+ super(cause?.message ?? "Database error");
108
+ this.name = "SqlError";
109
+ this.code = typeof cause?.code === "string" ? cause.code : null;
110
+ this.query = query;
111
+ this.position = Number.isInteger(cause?.position) ? cause.position : null;
112
+ }
113
+ }
114
+
115
+ export function query(sql, parameterNames) {
116
+ const parameters = Object.create(null);
117
+ let rows = [];
118
+ let cursor = -1;
119
+ let result = { changes: null, lastInsertRowid: null };
120
+ const api = {
121
+ execute() {
122
+ const adapter = requireDatabase();
123
+ const boundParameters = Object.fromEntries(
124
+ Object.entries(parameters).map(([name, value]) => [
125
+ name,
126
+ isDecimal(value) ? value.toFixed() : value,
127
+ ]),
128
+ );
129
+ try {
130
+ result = adapter.execute(sql, boundParameters);
131
+ } catch (error) {
132
+ if (error?.transactionUnusable === true && adapter.transactionState() !== null)
133
+ failedTransaction = true;
134
+ throw new SqlError(error, sql);
135
+ }
136
+ rows = result.rows;
137
+ cursor = -1;
138
+ return proxy;
139
+ },
140
+ next() {
141
+ if (cursor + 1 >= rows.length) {
142
+ cursor = rows.length;
143
+ return false;
144
+ }
145
+ cursor += 1;
146
+ return true;
147
+ },
148
+ next_result() { return false; },
149
+ };
150
+ const proxy = new Proxy(api, {
151
+ get(target, name) {
152
+ if (name === "affected_rows") return result.changes;
153
+ if (name === "generated_key") return result.lastInsertRowid;
154
+ if (name in target) return target[name];
155
+ if (cursor < 0 || cursor >= rows.length) throw new Error("SqlNoCurrentRow");
156
+ return rows[cursor][name] ?? null;
157
+ },
158
+ set(target, name, value) {
159
+ if (!parameterNames.includes(name)) throw new Error("SqlUnknownParameter");
160
+ parameters[name] = value;
161
+ return true;
162
+ },
163
+ });
164
+ return proxy;
165
+ }
166
+
167
+ export function translate(template, values) {
168
+ const translated = catalog[template] ?? template;
169
+
170
+ return translated.replace(/\\{(\\d+)\\}/g, (_, index) => String(values[index]));
171
+ }
172
+
173
+ export function xmlEscape(value) {
174
+ return String(value)
175
+ .replaceAll("&", "&amp;")
176
+ .replaceAll("<", "&lt;")
177
+ .replaceAll(">", "&gt;")
178
+ .replaceAll('"', "&quot;")
179
+ .replaceAll("'", "&apos;");
180
+ }
181
+
182
+ export function println(value) {
183
+ if (output === null) {
184
+ throw new Error("Lango runtime output is not configured");
185
+ }
186
+
187
+ output(String(value));
188
+ return null;
189
+ }
190
+
191
+ export function fromJson(text) {
192
+ try {
193
+ return decimalJson(JSON.parse(text));
194
+ } catch (error) {
195
+ throw new Error(\`JsonParseError: \${error.message}\`);
196
+ }
197
+ }
198
+
199
+ function decimalJson(value) {
200
+ if (Array.isArray(value)) return value.map(decimalJson);
201
+ if (value === null || typeof value !== "object") return value;
202
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
203
+ key,
204
+ key.startsWith("$") && typeof item === "number" ? decimal(item) : decimalJson(item),
205
+ ]));
206
+ }
207
+
208
+ export function toJson(value) {
209
+ try {
210
+ return JSON.stringify(jsonValue(value));
211
+ } catch (error) {
212
+ if (error.message === "NonFiniteNumber" || error.message === "JsonCycle") {
213
+ throw error;
214
+ }
215
+ throw new Error(\`JsonUnsupported: \${error.message}\`);
216
+ }
217
+ }
218
+
219
+ function jsonValue(value, ancestors = new Set()) {
220
+ if (value === undefined) return null;
221
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
222
+ return value;
223
+ }
224
+ if (isDecimal(value)) return JSON.rawJSON(value.toFixed());
225
+ if (typeof value === "number") {
226
+ if (!Number.isFinite(value)) throw new Error("NonFiniteNumber");
227
+ return value;
228
+ }
229
+ if (typeof value === "function" || typeof value !== "object") {
230
+ throw new Error("unsupported value");
231
+ }
232
+ if (ancestors.has(value)) throw new Error("JsonCycle");
233
+
234
+ ancestors.add(value);
235
+ try {
236
+ if (Array.isArray(value)) return value.map((item) => jsonValue(item, ancestors));
237
+ const prototype = Object.getPrototypeOf(value);
238
+ if (prototype !== Object.prototype && prototype !== null) {
239
+ throw new Error("unsupported value");
240
+ }
241
+ return Object.fromEntries(
242
+ Object.entries(value).map(([key, item]) => [key, jsonValue(item, ancestors)]),
243
+ );
244
+ } finally {
245
+ ancestors.delete(value);
246
+ }
247
+ }
248
+
249
+ export function getClass(value) {
250
+ if (value === null) return null;
251
+ if (typeof value === "number") return Number;
252
+ if (typeof value === "string") return String;
253
+ if (typeof value === "boolean") return Boolean;
254
+ return Object.getPrototypeOf(value)?.constructor ?? Object;
255
+ }
256
+
257
+ export function isKindof(value, Class) {
258
+ if (value === null) return false;
259
+ if (typeof value === "object" || typeof value === "function") return value instanceof Class;
260
+ return getClass(value) === Class;
261
+ }
262
+
263
+ export function isNumber(value) {
264
+ return typeof value === "number" && Number.isFinite(value);
265
+ }
266
+
267
+ export function isString(value) {
268
+ return typeof value === "string";
269
+ }
270
+
271
+ export function isBool(value) {
272
+ return typeof value === "boolean";
273
+ }
274
+
275
+ export function isObject(value) {
276
+ return value !== null && typeof value === "object" && !Array.isArray(value);
277
+ }
278
+
279
+ export function isArray(value) {
280
+ return Array.isArray(value);
281
+ }
282
+
283
+ export function includes(value, collection) {
284
+ if (Array.isArray(collection)) return collection.includes(value);
285
+ if (collection instanceof Set) return collection.has(value);
286
+ if (collection instanceof Map) return collection.has(value);
287
+ if (typeof collection === "string" && typeof value === "string") {
288
+ return collection.includes(value);
289
+ }
290
+ if (collection !== null && typeof collection === "object" && typeof value === "string") {
291
+ return Object.hasOwn(collection, value);
292
+ }
293
+ throw new Error("MembershipTarget");
294
+ }
295
+
296
+ export function entries(collection) {
297
+ if (collection instanceof Map) return collection.entries();
298
+ if (collection instanceof Set) return collection.entries();
299
+ if (Array.isArray(collection)) return collection.entries();
300
+ if (typeof collection === "string") return Array.from(collection).entries();
301
+ if (collection !== null && typeof collection?.[Symbol.iterator] === "function") {
302
+ return Array.from(collection).entries();
303
+ }
304
+ if (collection !== null && typeof collection === "object") {
305
+ return Object.entries(collection);
306
+ }
307
+ throw new Error("EntriesTarget");
308
+ }
309
+
310
+ export function* range(start, end, step = 1) {
311
+ if (
312
+ !Number.isSafeInteger(start) ||
313
+ !Number.isSafeInteger(end) ||
314
+ !Number.isSafeInteger(step) ||
315
+ step === 0
316
+ ) {
317
+ throw new Error("RangeBounds");
318
+ }
319
+ for (
320
+ let value = start;
321
+ step > 0 ? value < end : value > end;
322
+ value += step
323
+ ) yield value;
324
+ }
325
+
326
+ export function add(value, ...items) {
327
+ if (Array.isArray(value)) return value.push(...items);
328
+ if (value !== null && typeof value?.add === "function") {
329
+ return normalize(value.add(...items));
330
+ }
331
+ throw new Error("NotAddable");
332
+ }
333
+
334
+ export function remove(value, item) {
335
+ if (Array.isArray(value)) {
336
+ const index = value.indexOf(item);
337
+ if (index < 0) return false;
338
+ value.splice(index, 1);
339
+ return true;
340
+ }
341
+ if (value !== null && typeof value?.remove === "function") {
342
+ return normalize(value.remove(item));
343
+ }
344
+ throw new Error("NotRemovable");
345
+ }
346
+
347
+ export function remove_all(value, item) {
348
+ if (Array.isArray(value)) {
349
+ let removed = 0;
350
+ for (let index = value.length - 1; index >= 0; index -= 1) {
351
+ if (value[index] !== item) continue;
352
+ value.splice(index, 1);
353
+ removed += 1;
354
+ }
355
+ return removed;
356
+ }
357
+ if (value !== null && typeof value?.remove_all === "function") {
358
+ return normalize(value.remove_all(item));
359
+ }
360
+ throw new Error("NotRemovable");
361
+ }
362
+
363
+ export function clear(value) {
364
+ if (Array.isArray(value)) {
365
+ value.length = 0;
366
+ return null;
367
+ }
368
+ if (value !== null && typeof value?.clear === "function") {
369
+ return normalize(value.clear());
370
+ }
371
+ throw new Error("NotClearable");
372
+ }
373
+
374
+ export function remove_at(value, index) {
375
+ if (Array.isArray(value)) {
376
+ if (!Number.isSafeInteger(index) || index < 0 || index >= value.length)
377
+ return false;
378
+ value.splice(index, 1);
379
+ return true;
380
+ }
381
+ if (value !== null && typeof value?.remove_at === "function") {
382
+ return normalize(value.remove_at(index));
383
+ }
384
+ throw new Error("NotRemovable");
385
+ }
386
+
387
+ export function insert_at(value, index, item) {
388
+ if (Array.isArray(value)) {
389
+ if (!Number.isSafeInteger(index) || index < 0 || index > value.length)
390
+ return false;
391
+ value.splice(index, 0, item);
392
+ return true;
393
+ }
394
+ if (value !== null && typeof value?.insert_at === "function") {
395
+ return normalize(value.insert_at(index, item));
396
+ }
397
+ throw new Error("NotInsertable");
398
+ }
399
+
400
+ export function optional(value, callback) {
401
+ return value === null || value === undefined ? null : callback(value);
402
+ }
403
+
404
+ export function requireBoolean(value) {
405
+ if (typeof value !== "boolean") {
406
+ throw new Error("BooleanRequired");
407
+ }
408
+
409
+ return value;
410
+ }
411
+
412
+ export function assert(value, location) {
413
+ if (!value) throw new Error(\`AssertionFailed at \${location}\`);
414
+ return null;
415
+ }
416
+
417
+ export function normalize(value) {
418
+ if (value === undefined) return null;
419
+ if (typeof value === "number" && !Number.isFinite(value)) {
420
+ throw new Error("NonFiniteNumber");
421
+ }
422
+ return value;
423
+ }
424
+
425
+ export function finite(value) {
426
+ if (typeof value === "number" && !Number.isFinite(value)) {
427
+ throw new Error("NonFiniteNumber");
428
+ }
429
+ return value;
430
+ }
431
+
432
+ export function requireNumber(value) {
433
+ if (typeof value !== "number") throw new Error("NumberRequired");
434
+ return finite(value);
435
+ }
436
+
437
+ export function call(value, ...arguments_) {
438
+ if (typeof value !== "function") throw new Error("NotCallable");
439
+ return normalize(value(...arguments_));
440
+ }
441
+
442
+ export function boundary(value) {
443
+ return finite(normalize(value));
444
+ }
445
+
446
+ export function requireArity(actual, requiredPositions, name) {
447
+ const missing = requiredPositions.find((position) => actual <= position);
448
+ if (missing !== undefined) {
449
+ throw new Error(\`ArityError: \${name} is missing required argument \${missing + 1}\`);
450
+ }
451
+ return null;
452
+ }
453
+
454
+ export function withArity(callback, requiredPositions, name) {
455
+ return function (...arguments_) {
456
+ requireArity(arguments_.length, requiredPositions, name);
457
+ return callback(...arguments_);
458
+ };
459
+ }
460
+
461
+ export function contextGet(name) {
462
+ const stack = contexts.get(name);
463
+ if (!stack || stack.length === 0) {
464
+ throw new Error(\`Lango context is unavailable: \${name}\`);
465
+ }
466
+
467
+ return stack.at(-1);
468
+ }
469
+
470
+ export function contextSet(name, value) {
471
+ const stack = contexts.get(name) ?? [];
472
+ contexts.set(name, stack);
473
+ stack.push(value);
474
+ return name;
475
+ }
476
+
477
+ export function contextRestore(names) {
478
+ for (const name of names.reverse()) {
479
+ const stack = contexts.get(name);
480
+ stack.pop();
481
+ if (stack.length === 0) contexts.delete(name);
482
+ }
483
+ }
484
+ `;
485
+ }
486
+
487
+ export const RUNTIME_SOURCE = runtimeSource();
@@ -0,0 +1,5 @@
1
+ {
2
+ "recommendations": [
3
+ "lango-project.lango"
4
+ ]
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "[lango]": {
3
+ "editor.formatOnSave": true
4
+ }
5
+ }
@@ -0,0 +1,18 @@
1
+ # Lango application
2
+
3
+ ## Start
4
+
5
+ ```sh
6
+ npm install
7
+ npm run run
8
+ ```
9
+
10
+ ## Commands
11
+
12
+ * `npm run build` compiles `src/main.lango` to `dist/main.mjs`.
13
+ * `npm run run` runs the Lango entry point.
14
+ * `npm run test` runs Lango test blocks under `test/`.
15
+ * `npm run format` formats the starter source file.
16
+
17
+ Install the **Lango** VS Code extension for syntax highlighting, diagnostics,
18
+ and document formatting.
@@ -0,0 +1,2 @@
1
+ node_modules/
2
+ dist/
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "lango-application",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "lango build src/main.lango --out dist/main.mjs",
8
+ "run": "lango run src/main.lango",
9
+ "test": "lango test test/main.lango",
10
+ "format": "lango format src/main.lango"
11
+ },
12
+ "devDependencies": {
13
+ "@lango-project/lango-js": "{{LANGO_VERSION}}"
14
+ },
15
+ "engines": {
16
+ "node": ">=24"
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ export main(args)
2
+
3
+ main(args)
4
+ println("Hello from Lango")
@@ -0,0 +1,2 @@
1
+ test "starter test"
2
+ assert true