rip-lang 3.15.4 → 3.16.1
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/README.md +6 -4
- package/bin/rip +167 -12
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +808 -0
- package/docs/RIP-DUCKDB.md +477 -0
- package/docs/RIP-INTRO.md +396 -0
- package/docs/RIP-LANG.md +59 -5
- package/docs/RIP-SCHEMA.md +191 -8
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +3627 -1470
- package/docs/dist/rip.min.js +671 -244
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +7 -7
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/duckdb/manifest.json +1 -1
- package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/vscode/print/index.html +2 -1
- package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/ui/bundle.json +61 -0
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +0 -7
- package/docs/ui/index.css +66 -23
- package/docs/ui/index.html +6 -6
- package/package.json +9 -3
- package/rip-loader.js +64 -2
- package/src/AGENTS.md +63 -36
- package/src/browser.js +96 -14
- package/src/compiler.js +960 -143
- package/src/components.js +794 -88
- package/src/{types-emit.js → dts.js} +181 -71
- package/src/grammar/README.md +1 -1
- package/src/grammar/grammar.rip +111 -97
- package/src/lexer.js +132 -18
- package/src/parser.js +203 -205
- package/src/repl.js +74 -6
- package/src/schema/runtime-orm.js +168 -4
- package/src/schema/runtime-validate.js +146 -2
- package/src/schema/runtime.generated.js +314 -6
- package/src/schema/schema.js +5 -5
- package/src/sourcemaps.js +277 -1
- package/src/stdlib.js +253 -0
- package/src/typecheck.js +2023 -106
- package/src/types.js +127 -7
- package/docs/ui/accordion.rip +0 -103
- package/docs/ui/alert-dialog.rip +0 -53
- package/docs/ui/autocomplete.rip +0 -115
- package/docs/ui/avatar.rip +0 -37
- package/docs/ui/badge.rip +0 -15
- package/docs/ui/breadcrumb.rip +0 -47
- package/docs/ui/button-group.rip +0 -26
- package/docs/ui/button.rip +0 -23
- package/docs/ui/card.rip +0 -25
- package/docs/ui/carousel.rip +0 -110
- package/docs/ui/checkbox-group.rip +0 -61
- package/docs/ui/checkbox.rip +0 -33
- package/docs/ui/collapsible.rip +0 -50
- package/docs/ui/combobox.rip +0 -130
- package/docs/ui/context-menu.rip +0 -88
- package/docs/ui/date-picker.rip +0 -206
- package/docs/ui/dialog.rip +0 -60
- package/docs/ui/drawer.rip +0 -58
- package/docs/ui/editable-value.rip +0 -82
- package/docs/ui/field.rip +0 -53
- package/docs/ui/fieldset.rip +0 -22
- package/docs/ui/form.rip +0 -39
- package/docs/ui/grid.rip +0 -901
- package/docs/ui/input-group.rip +0 -28
- package/docs/ui/input.rip +0 -36
- package/docs/ui/label.rip +0 -16
- package/docs/ui/menu.rip +0 -134
- package/docs/ui/menubar.rip +0 -151
- package/docs/ui/meter.rip +0 -36
- package/docs/ui/multi-select.rip +0 -203
- package/docs/ui/native-select.rip +0 -33
- package/docs/ui/nav-menu.rip +0 -126
- package/docs/ui/number-field.rip +0 -162
- package/docs/ui/otp-field.rip +0 -89
- package/docs/ui/pagination.rip +0 -123
- package/docs/ui/popover.rip +0 -93
- package/docs/ui/preview-card.rip +0 -75
- package/docs/ui/progress.rip +0 -25
- package/docs/ui/radio-group.rip +0 -57
- package/docs/ui/resizable.rip +0 -123
- package/docs/ui/scroll-area.rip +0 -145
- package/docs/ui/select.rip +0 -151
- package/docs/ui/separator.rip +0 -17
- package/docs/ui/skeleton.rip +0 -22
- package/docs/ui/slider.rip +0 -165
- package/docs/ui/spinner.rip +0 -17
- package/docs/ui/table.rip +0 -27
- package/docs/ui/tabs.rip +0 -113
- package/docs/ui/textarea.rip +0 -48
- package/docs/ui/toast.rip +0 -87
- package/docs/ui/toggle-group.rip +0 -71
- package/docs/ui/toggle.rip +0 -24
- package/docs/ui/toolbar.rip +0 -38
- package/docs/ui/tooltip.rip +0 -85
- package/src/app.rip +0 -1571
- package/src/sourcemap-merge.js +0 -287
- /package/docs/demo/{components → routes}/_layout.rip +0 -0
- /package/docs/demo/{components → routes}/about.rip +0 -0
- /package/docs/demo/{components → routes}/card.rip +0 -0
- /package/docs/demo/{components → routes}/counter.rip +0 -0
- /package/docs/demo/{components → routes}/index.rip +0 -0
- /package/docs/demo/{components → routes}/todos.rip +0 -0
- /package/src/schema/{dts-emit.js → dts.js} +0 -0
package/src/sourcemaps.js
CHANGED
|
@@ -198,4 +198,280 @@ function buildLineMap(reverseMap, mapJSON, headerLines) {
|
|
|
198
198
|
return { srcToGen, genToSrc, srcColToGen };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
// =============================================================================
|
|
202
|
+
// Multi-chunk merger.
|
|
203
|
+
//
|
|
204
|
+
// Concatenates N JS chunks (each with its own inline source map) into one
|
|
205
|
+
// JS string + one merged source map. Used by `processRipScripts` in
|
|
206
|
+
// browser.js to produce a single eval'd chunk that DevTools can map back
|
|
207
|
+
// to multiple original `.rip` files.
|
|
208
|
+
//
|
|
209
|
+
// Why this exists: DevTools only honours the LAST `//# sourceMappingURL`
|
|
210
|
+
// inside one eval'd chunk. So when N chunks share lexical scope (concat
|
|
211
|
+
// + single eval, the bundle-no-router execution model), we must produce
|
|
212
|
+
// ONE map that covers all N sources. Source Map V3 supports this natively
|
|
213
|
+
// via a multi-entry `sources` / `sourcesContent` array and source-index
|
|
214
|
+
// fields in the VLQ mappings — every production JS bundler does this.
|
|
215
|
+
//
|
|
216
|
+
// Algorithm:
|
|
217
|
+
// 1. Strip per-chunk `//# sourceMappingURL=` pragmas from each chunk's JS
|
|
218
|
+
// 2. Decode each chunk's `mappings` to absolute coordinates
|
|
219
|
+
// 3. Concatenate JS with `\n;\n` separators (statement-safe, +2 newlines
|
|
220
|
+
// between chunks)
|
|
221
|
+
// 4. Offset each chunk's generated line numbers by the cumulative line
|
|
222
|
+
// count of preceding chunks + separators
|
|
223
|
+
// 5. Dedupe `sources`, `sourcesContent`, `names` into merged arrays;
|
|
224
|
+
// remap each chunk's source-index / name-index fields
|
|
225
|
+
// 6. Re-encode as relative VLQ across the full concatenated mapping
|
|
226
|
+
// =============================================================================
|
|
227
|
+
|
|
228
|
+
// `\n;\n` is statement-safe between chunks (the `;` ensures the previous
|
|
229
|
+
// line's expression is terminated). Counts as 2 newlines for line-offset.
|
|
230
|
+
const MERGE_SEPARATOR = '\n;\n';
|
|
231
|
+
const MERGE_SEPARATOR_LINES = 2;
|
|
232
|
+
|
|
233
|
+
// Strip a trailing `//# sourceMappingURL=...` pragma from a JS string so
|
|
234
|
+
// concatenated chunks don't carry stray maps that DevTools would honor
|
|
235
|
+
// in unexpected order. Returns { js, mapJSON } where mapJSON is the
|
|
236
|
+
// JSON string extracted from the inline data URL (or null if no pragma).
|
|
237
|
+
function extractInlineMap(js) {
|
|
238
|
+
const re = /\n?\/\/# sourceMappingURL=data:application\/json(?:;charset=[^;,]+)?;base64,([A-Za-z0-9+/=]+)\s*$/;
|
|
239
|
+
const m = js.match(re);
|
|
240
|
+
if (!m) return { js, mapJSON: null };
|
|
241
|
+
let mapJSON;
|
|
242
|
+
try {
|
|
243
|
+
// UTF-8-safe decode (counterpart of the encode in compiler.js).
|
|
244
|
+
const bin = atob(m[1]);
|
|
245
|
+
const bytes = new Uint8Array(bin.length);
|
|
246
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
247
|
+
mapJSON = new TextDecoder().decode(bytes);
|
|
248
|
+
} catch {
|
|
249
|
+
return { js: js.slice(0, m.index), mapJSON: null };
|
|
250
|
+
}
|
|
251
|
+
return { js: js.slice(0, m.index), mapJSON };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Count the number of newline characters in a string. Used for line-offset
|
|
255
|
+
// math when concatenating chunks.
|
|
256
|
+
function countNewlines(str) {
|
|
257
|
+
let n = 0;
|
|
258
|
+
for (let i = 0; i < str.length; i++) if (str.charCodeAt(i) === 10) n++;
|
|
259
|
+
return n;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Decode a Source Map V3 `mappings` field into an array of arrays of
|
|
263
|
+
// absolute-coordinate segments. Each segment is one of:
|
|
264
|
+
// { genCol } — unmapped (1 field)
|
|
265
|
+
// { genCol, src, origLine, origCol } — mapped (4 fields)
|
|
266
|
+
// { genCol, src, origLine, origCol, name } — mapped + name (5 fields)
|
|
267
|
+
function decodeMappings(mappingsStr) {
|
|
268
|
+
const lines = mappingsStr.split(';');
|
|
269
|
+
const result = [];
|
|
270
|
+
let src = 0, origLine = 0, origCol = 0, nameIdx = 0;
|
|
271
|
+
for (const line of lines) {
|
|
272
|
+
const segs = [];
|
|
273
|
+
let genCol = 0;
|
|
274
|
+
if (line.length > 0) {
|
|
275
|
+
for (const segStr of line.split(',')) {
|
|
276
|
+
if (segStr.length === 0) continue;
|
|
277
|
+
const fields = vlqDecode(segStr);
|
|
278
|
+
genCol += fields[0];
|
|
279
|
+
if (fields.length === 1) {
|
|
280
|
+
segs.push({ genCol });
|
|
281
|
+
} else {
|
|
282
|
+
src += fields[1];
|
|
283
|
+
origLine += fields[2];
|
|
284
|
+
origCol += fields[3];
|
|
285
|
+
const seg = { genCol, src, origLine, origCol };
|
|
286
|
+
if (fields.length >= 5) {
|
|
287
|
+
nameIdx += fields[4];
|
|
288
|
+
seg.name = nameIdx;
|
|
289
|
+
}
|
|
290
|
+
segs.push(seg);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
result.push(segs);
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Re-encode an array of arrays of absolute-coordinate segments back to
|
|
300
|
+
// V3 `mappings` string. Counterpart of decodeMappings.
|
|
301
|
+
function encodeMappings(perLineSegs) {
|
|
302
|
+
let prevGenCol = 0; // resets per line
|
|
303
|
+
let prevSrc = 0, prevOrigLine = 0, prevOrigCol = 0, prevNameIdx = 0;
|
|
304
|
+
const out = [];
|
|
305
|
+
for (const segs of perLineSegs) {
|
|
306
|
+
prevGenCol = 0;
|
|
307
|
+
const lineParts = [];
|
|
308
|
+
for (const seg of segs) {
|
|
309
|
+
if (seg.src == null) {
|
|
310
|
+
lineParts.push(vlqEncode(seg.genCol - prevGenCol));
|
|
311
|
+
} else {
|
|
312
|
+
let s = vlqEncode(seg.genCol - prevGenCol);
|
|
313
|
+
s += vlqEncode(seg.src - prevSrc);
|
|
314
|
+
s += vlqEncode(seg.origLine - prevOrigLine);
|
|
315
|
+
s += vlqEncode(seg.origCol - prevOrigCol);
|
|
316
|
+
if (seg.name != null) {
|
|
317
|
+
s += vlqEncode(seg.name - prevNameIdx);
|
|
318
|
+
prevNameIdx = seg.name;
|
|
319
|
+
}
|
|
320
|
+
prevSrc = seg.src;
|
|
321
|
+
prevOrigLine = seg.origLine;
|
|
322
|
+
prevOrigCol = seg.origCol;
|
|
323
|
+
lineParts.push(s);
|
|
324
|
+
}
|
|
325
|
+
prevGenCol = seg.genCol;
|
|
326
|
+
}
|
|
327
|
+
out.push(lineParts.join(','));
|
|
328
|
+
}
|
|
329
|
+
return out.join(';');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Given an array of `{js, mapJSON}` chunks (where mapJSON is the source-map
|
|
333
|
+
// V3 JSON string for that chunk, or null/undefined if the chunk has no
|
|
334
|
+
// map), produce a single merged result.
|
|
335
|
+
//
|
|
336
|
+
// Returns:
|
|
337
|
+
// {
|
|
338
|
+
// js, // concatenated JS, with all per-chunk sourceMappingURL
|
|
339
|
+
// // pragmas stripped — caller appends ONE merged pragma
|
|
340
|
+
// mapJSON, // V3 source-map JSON string covering all chunks, or
|
|
341
|
+
// // null if no chunk had a map
|
|
342
|
+
// }
|
|
343
|
+
//
|
|
344
|
+
// The chunks must be in the order they should be eval'd. Each chunk's JS
|
|
345
|
+
// is treated as opaque — we don't parse it, we only count newlines for
|
|
346
|
+
// line-offset math.
|
|
347
|
+
function mergeChunks(chunks) {
|
|
348
|
+
// Strip per-chunk pragmas first; we'll append exactly one at the end.
|
|
349
|
+
const stripped = chunks.map((c) => {
|
|
350
|
+
if (c.mapJSON !== undefined) return { js: c.js, mapJSON: c.mapJSON };
|
|
351
|
+
return extractInlineMap(c.js);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// If no chunk has a map, the merged JS is just concatenated; no map.
|
|
355
|
+
const anyMap = stripped.some((c) => c.mapJSON);
|
|
356
|
+
const mergedJS = stripped.map((c) => c.js).join(MERGE_SEPARATOR);
|
|
357
|
+
if (!anyMap) return { js: mergedJS, mapJSON: null };
|
|
358
|
+
|
|
359
|
+
// Build merged sources / sourcesContent / names with deduplication
|
|
360
|
+
// by string identity. Per-chunk remap tables let us rewrite each
|
|
361
|
+
// chunk's source-index / name-index fields into the merged arrays.
|
|
362
|
+
const mergedSources = [];
|
|
363
|
+
const mergedSourcesContent = [];
|
|
364
|
+
const sourceIndexByName = new Map();
|
|
365
|
+
const mergedNames = [];
|
|
366
|
+
const nameIndexByName = new Map();
|
|
367
|
+
|
|
368
|
+
function addSource(name, content) {
|
|
369
|
+
if (sourceIndexByName.has(name)) return sourceIndexByName.get(name);
|
|
370
|
+
const idx = mergedSources.length;
|
|
371
|
+
mergedSources.push(name);
|
|
372
|
+
mergedSourcesContent.push(content ?? null);
|
|
373
|
+
sourceIndexByName.set(name, idx);
|
|
374
|
+
return idx;
|
|
375
|
+
}
|
|
376
|
+
function addName(name) {
|
|
377
|
+
if (nameIndexByName.has(name)) return nameIndexByName.get(name);
|
|
378
|
+
const idx = mergedNames.length;
|
|
379
|
+
mergedNames.push(name);
|
|
380
|
+
nameIndexByName.set(name, idx);
|
|
381
|
+
return idx;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Per-chunk: line offset (cumulative gen-line shift), source-index
|
|
385
|
+
// remap, name-index remap, decoded segments.
|
|
386
|
+
const chunkInfos = [];
|
|
387
|
+
let lineOffset = 0;
|
|
388
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
389
|
+
const c = stripped[i];
|
|
390
|
+
const info = { lineOffset, srcRemap: [], nameRemap: [], perLineSegs: null };
|
|
391
|
+
|
|
392
|
+
if (c.mapJSON) {
|
|
393
|
+
let map;
|
|
394
|
+
try { map = JSON.parse(c.mapJSON); }
|
|
395
|
+
catch { map = null; }
|
|
396
|
+
if (map && typeof map.mappings === 'string') {
|
|
397
|
+
const sources = map.sources || [];
|
|
398
|
+
const sourcesContent = map.sourcesContent || [];
|
|
399
|
+
info.srcRemap = sources.map((s, idx) => addSource(s, sourcesContent[idx]));
|
|
400
|
+
const names = map.names || [];
|
|
401
|
+
info.nameRemap = names.map((n) => addName(n));
|
|
402
|
+
info.perLineSegs = decodeMappings(map.mappings);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
chunkInfos.push(info);
|
|
407
|
+
|
|
408
|
+
// Advance lineOffset by this chunk's line count + separator (except
|
|
409
|
+
// after the last chunk).
|
|
410
|
+
const chunkLines = countNewlines(c.js);
|
|
411
|
+
const sepLines = (i < stripped.length - 1) ? MERGE_SEPARATOR_LINES : 0;
|
|
412
|
+
lineOffset += chunkLines + sepLines;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Walk all chunks and emit their segments into the merged per-line
|
|
416
|
+
// segment array, with each chunk's gen-line indices shifted by the
|
|
417
|
+
// chunk's lineOffset and source/name indices remapped.
|
|
418
|
+
const totalLines = lineOffset + 1;
|
|
419
|
+
const mergedPerLineSegs = new Array(totalLines).fill(null).map(() => []);
|
|
420
|
+
|
|
421
|
+
for (const info of chunkInfos) {
|
|
422
|
+
if (!info.perLineSegs) continue;
|
|
423
|
+
for (let li = 0; li < info.perLineSegs.length; li++) {
|
|
424
|
+
const target = info.lineOffset + li;
|
|
425
|
+
if (target >= mergedPerLineSegs.length) {
|
|
426
|
+
while (mergedPerLineSegs.length <= target) mergedPerLineSegs.push([]);
|
|
427
|
+
}
|
|
428
|
+
for (const seg of info.perLineSegs[li]) {
|
|
429
|
+
if (seg.src == null) {
|
|
430
|
+
mergedPerLineSegs[target].push({ genCol: seg.genCol });
|
|
431
|
+
} else {
|
|
432
|
+
const remapped = {
|
|
433
|
+
genCol: seg.genCol,
|
|
434
|
+
src: info.srcRemap[seg.src],
|
|
435
|
+
origLine: seg.origLine,
|
|
436
|
+
origCol: seg.origCol,
|
|
437
|
+
};
|
|
438
|
+
if (seg.name != null) remapped.name = info.nameRemap[seg.name];
|
|
439
|
+
mergedPerLineSegs[target].push(remapped);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const mergedMappings = encodeMappings(mergedPerLineSegs);
|
|
446
|
+
const mergedMap = {
|
|
447
|
+
version: 3,
|
|
448
|
+
sources: mergedSources,
|
|
449
|
+
sourcesContent: mergedSourcesContent,
|
|
450
|
+
names: mergedNames,
|
|
451
|
+
mappings: mergedMappings,
|
|
452
|
+
};
|
|
453
|
+
return { js: mergedJS, mapJSON: JSON.stringify(mergedMap) };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// UTF-8-safe base64 encode for embedding mapJSON as a data URL pragma.
|
|
457
|
+
function utf8ToBase64(str) {
|
|
458
|
+
if (typeof Buffer !== 'undefined') {
|
|
459
|
+
return Buffer.from(str, 'utf8').toString('base64');
|
|
460
|
+
}
|
|
461
|
+
const bytes = new TextEncoder().encode(str);
|
|
462
|
+
let bin = '';
|
|
463
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
464
|
+
return btoa(bin);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Convenience: take chunks, run mergeChunks, append a single
|
|
468
|
+
// `//# sourceMappingURL=` data-URL pragma to the merged JS, return it.
|
|
469
|
+
// If no chunk had a map, returns the concat'd JS unchanged.
|
|
470
|
+
function mergeChunksWithInlineMap(chunks) {
|
|
471
|
+
const { js, mapJSON } = mergeChunks(chunks);
|
|
472
|
+
if (!mapJSON) return js;
|
|
473
|
+
const b64 = utf8ToBase64(mapJSON);
|
|
474
|
+
return js + '\n//# sourceMappingURL=data:application/json;base64,' + b64 + '\n';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export { SourceMapGenerator, vlqEncode, vlqDecode, parseSourceMap, buildLineMap, mergeChunks, mergeChunksWithInlineMap };
|
package/src/stdlib.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// stdlib.js — Rip's standard library
|
|
3
|
+
//
|
|
4
|
+
// One file, three concerns, all related to the small set of helpers Rip
|
|
5
|
+
// makes available without an explicit import:
|
|
6
|
+
//
|
|
7
|
+
// 1. `getStdlibCode()`
|
|
8
|
+
// Returns a string of self-contained JavaScript that the compiler
|
|
9
|
+
// prepends to every emitted file. Defines the `globalThis.*` helpers
|
|
10
|
+
// every Rip program can rely on (p, pp, pr, pj, kind, abort, …).
|
|
11
|
+
// The string is bracketed with `// rip:stdlib:begin` / `:end` markers
|
|
12
|
+
// so tooling (test runner, source viewers) can strip the preamble
|
|
13
|
+
// cleanly even when individual helper bodies span multiple lines.
|
|
14
|
+
//
|
|
15
|
+
// 2. `stringify(value, opts?)`
|
|
16
|
+
// The importable, *strict* JS-value -> Rip-syntax serializer used
|
|
17
|
+
// from `import { stringify } from 'rip-lang'`. Throws on values
|
|
18
|
+
// that have no faithful Rip-literal form (Date, BigInt, Map, Set,
|
|
19
|
+
// class instances, functions, non-interned symbols), preserving the
|
|
20
|
+
// round-trip property: `compile(stringify(v))` returns `v`.
|
|
21
|
+
//
|
|
22
|
+
// 3. `STDLIB_TYPE_DECLS`
|
|
23
|
+
// A map from helper name to TypeScript declaration string. The
|
|
24
|
+
// typecheck pipeline injects these into shadow `.d.ts` so user code
|
|
25
|
+
// that calls `assert(x)`, `kind(v)`, etc. type-checks under the
|
|
26
|
+
// strict configuration. Co-located here so adding a stdlib helper
|
|
27
|
+
// is a one-file change — both the runtime body and the TS
|
|
28
|
+
// declaration live in this module.
|
|
29
|
+
//
|
|
30
|
+
// The runtime `pr` formatter inlined in `getStdlibCode()` is intentionally
|
|
31
|
+
// duplicated with `stringify` rather than refactored through Function-
|
|
32
|
+
// constructor indirection. Two visible function bodies in one file are
|
|
33
|
+
// easier to keep in sync than one source threaded through `eval`-style
|
|
34
|
+
// evaluation. If they ever diverge the diff will be local and obvious.
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
// -----------------------------------------------------------------------------
|
|
38
|
+
// 1. Runtime stdlib injection (string-literal JS, NOT executed in this scope)
|
|
39
|
+
// -----------------------------------------------------------------------------
|
|
40
|
+
//
|
|
41
|
+
// IMPORTANT: this string is not parsed by the JS engine here — it is
|
|
42
|
+
// literally prepended to other compiled files as text. It cannot reference
|
|
43
|
+
// anything from this module's scope. It must be self-contained.
|
|
44
|
+
//
|
|
45
|
+
// The `// rip:stdlib:begin` / `// rip:stdlib:end` markers are LOAD-BEARING.
|
|
46
|
+
// `test/runner.js` strips everything between them in `normalizeCode()`
|
|
47
|
+
// before comparing compiled output against expected JS in `code` tests.
|
|
48
|
+
// Removing or renaming these markers without updating the runner will
|
|
49
|
+
// silently break every `code` snapshot test in the suite.
|
|
50
|
+
//
|
|
51
|
+
// `pr` here mirrors the strict `stringify()` defined later in this file,
|
|
52
|
+
// with one deliberate difference: `pr` returns null from its inner
|
|
53
|
+
// formatter for any value that has no Rip-literal form (class instance,
|
|
54
|
+
// Date, BigInt, Map, Set, function, non-interned Symbol) and the outer
|
|
55
|
+
// wrapper falls back to `console.dir` for those values, so a debug
|
|
56
|
+
// session never crashes on whatever the caller happens to hand it. The
|
|
57
|
+
// strict version throws so round-trip-safe consumers can detect the
|
|
58
|
+
// problem at call time. Keep the formatting logic in sync with the
|
|
59
|
+
// `stringify` body below — same key/string quoting rules, same array
|
|
60
|
+
// and object shapes, same symbol literal form.
|
|
61
|
+
|
|
62
|
+
export function getStdlibCode() {
|
|
63
|
+
return `\
|
|
64
|
+
// rip:stdlib:begin
|
|
65
|
+
globalThis.abort ??= (msg) => { if (msg) console.error(msg); process.exit(1); };
|
|
66
|
+
globalThis.assert ??= (v, msg) => { if (!v) throw new Error(msg || "Assertion failed"); };
|
|
67
|
+
globalThis.exit ??= (code) => process.exit(code || 0);
|
|
68
|
+
globalThis.kind ??= (v) => v != null ? (v.constructor?.name || Object.prototype.toString.call(v).slice(8, -1)).toLowerCase() : String(v);
|
|
69
|
+
globalThis.noop ??= () => {};
|
|
70
|
+
globalThis.p ??= console.log;
|
|
71
|
+
globalThis.pp ??= (v) => { console.dir(v, { depth: null, colors: true }); return v; };
|
|
72
|
+
globalThis.pj ??= (v) => { console.log(JSON.stringify(v, null, 2)); return v; };
|
|
73
|
+
globalThis.pr ??= (() => {
|
|
74
|
+
const BARE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
75
|
+
const esc = (s, q) => s.replace(/\\\\/g, "\\\\\\\\").replace(/\\n/g, "\\\\n").replace(/\\r/g, "\\\\r").replace(/\\t/g, "\\\\t").replace(new RegExp(q, 'g'), "\\\\" + q);
|
|
76
|
+
const qs = (s) => !s.includes("'") ? "'" + esc(s, "'") + "'" : !s.includes('"') ? '"' + esc(s, '"').split("#{").join("\\\\#{") + '"' : "'" + esc(s, "'") + "'";
|
|
77
|
+
const ks = (k) => { if (typeof k === 'symbol') { const n = Symbol.keyFor(k); return n && BARE.test(n) ? ':' + n : qs(String(k)); } return BARE.test(k) ? k : qs(k); };
|
|
78
|
+
const fmt = (v, d) => {
|
|
79
|
+
if (v === null) return 'null';
|
|
80
|
+
if (v === undefined) return 'undefined';
|
|
81
|
+
const t = typeof v;
|
|
82
|
+
if (t === 'number' || t === 'boolean') return String(v);
|
|
83
|
+
if (t === 'string') return qs(v);
|
|
84
|
+
if (t === 'symbol') { const n = Symbol.keyFor(v); return n && BARE.test(n) ? ':' + n : null; }
|
|
85
|
+
if (Array.isArray(v)) {
|
|
86
|
+
if (v.length === 0) return '[]';
|
|
87
|
+
const pad = ' '.repeat(d + 1), end = ' '.repeat(d);
|
|
88
|
+
return '[\\n' + pad + v.map(x => fmt(x, d + 1) ?? 'null').join('\\n' + pad) + '\\n' + end + ']';
|
|
89
|
+
}
|
|
90
|
+
if (t === 'object') {
|
|
91
|
+
const proto = Object.getPrototypeOf(v);
|
|
92
|
+
if (proto !== Object.prototype && proto !== null) return null;
|
|
93
|
+
const keys = Object.keys(v);
|
|
94
|
+
if (keys.length === 0) return '{}';
|
|
95
|
+
const pad = ' '.repeat(d + 1), end = ' '.repeat(d);
|
|
96
|
+
return '{\\n' + pad + keys.map(k => ks(k) + ': ' + (fmt(v[k], d + 1) ?? 'null')).join('\\n' + pad) + '\\n' + end + '}';
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
return (v) => { const s = fmt(v, 0); s !== null ? console.log(s) : console.dir(v, { depth: null, colors: true }); return v; };
|
|
101
|
+
})();
|
|
102
|
+
globalThis.raise ??= (a, b) => { throw (b !== undefined ? new a(b) : new Error(a)); };
|
|
103
|
+
globalThis.rand ??= (a, b) => b !== undefined ? (a > b && ([a, b] = [b, a]), Math.floor(Math.random() * (b - a + 1) + a)) : a ? Math.floor(Math.random() * a) : Math.random();
|
|
104
|
+
globalThis.sleep ??= (ms) => new Promise(r => setTimeout(r, ms));
|
|
105
|
+
globalThis.todo ??= (msg) => { throw new Error(msg || "Not implemented"); };
|
|
106
|
+
globalThis.warn ??= console.warn;
|
|
107
|
+
globalThis.zip ??= (...a) => a[0].map((_, i) => a.map(b => b[i]));
|
|
108
|
+
// rip:stdlib:end
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -----------------------------------------------------------------------------
|
|
113
|
+
// 2. Importable, strict Rip-syntax serializer
|
|
114
|
+
// -----------------------------------------------------------------------------
|
|
115
|
+
//
|
|
116
|
+
// Public API. `import { stringify } from 'rip-lang'` is the supported
|
|
117
|
+
// surface for tools that emit `.rip` files programmatically (config
|
|
118
|
+
// migrations, scaffolders, code generators).
|
|
119
|
+
//
|
|
120
|
+
// Round-trip property: for any value the function accepts,
|
|
121
|
+
// `compile(stringify(v))` evaluates to a value structurally equal to `v`.
|
|
122
|
+
// That guarantee is preserved by REJECTING values with no faithful Rip-
|
|
123
|
+
// literal form rather than rendering them best-effort. The accepted set:
|
|
124
|
+
//
|
|
125
|
+
// * null, undefined
|
|
126
|
+
// * number, boolean, string
|
|
127
|
+
// * interned Symbol (`Symbol.for(name)` where `name` is bare-key shape)
|
|
128
|
+
// * plain Array (any depth, any accepted element types)
|
|
129
|
+
// * plain Object (own enumerable string keys, accepted values)
|
|
130
|
+
//
|
|
131
|
+
// Anything else throws `StringifyError`:
|
|
132
|
+
//
|
|
133
|
+
// * Date, BigInt, RegExp
|
|
134
|
+
// * Map, Set, WeakMap, WeakSet
|
|
135
|
+
// * class instances (anything with a non-Object prototype)
|
|
136
|
+
// * functions
|
|
137
|
+
// * non-interned Symbols
|
|
138
|
+
// * circular references (would already throw via stack overflow before
|
|
139
|
+
// the type check fires; explicit detection is a future enhancement)
|
|
140
|
+
//
|
|
141
|
+
// `opts.indent` accepts a string or a number-of-spaces; defaults to 2.
|
|
142
|
+
|
|
143
|
+
const BARE_KEY_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
144
|
+
|
|
145
|
+
class StringifyError extends Error {
|
|
146
|
+
constructor(msg) {
|
|
147
|
+
super(msg);
|
|
148
|
+
this.name = 'StringifyError';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Render a string. Single-quote by default; switch to double-quote when
|
|
153
|
+
// the value contains an apostrophe but no double-quote, and escape any
|
|
154
|
+
// `#{...}` sequences inside double-quoted output so Rip interpolation
|
|
155
|
+
// can't fire on the round-trip.
|
|
156
|
+
function quoteString(str) {
|
|
157
|
+
const hasSingle = str.includes("'");
|
|
158
|
+
const hasDouble = str.includes('"');
|
|
159
|
+
const escape = (s, q) =>
|
|
160
|
+
s
|
|
161
|
+
.replace(/\\/g, '\\\\')
|
|
162
|
+
.replace(/\n/g, '\\n')
|
|
163
|
+
.replace(/\r/g, '\\r')
|
|
164
|
+
.replace(/\t/g, '\\t')
|
|
165
|
+
.replace(new RegExp(q, 'g'), `\\${q}`);
|
|
166
|
+
if (!hasSingle) return `'${escape(str, "'")}'`;
|
|
167
|
+
if (!hasDouble) {
|
|
168
|
+
let body = escape(str, '"').split('#{').join('\\#{');
|
|
169
|
+
return `"${body}"`;
|
|
170
|
+
}
|
|
171
|
+
return `'${escape(str, "'")}'`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatKey(key) {
|
|
175
|
+
if (typeof key === 'symbol') {
|
|
176
|
+
const name = Symbol.keyFor(key);
|
|
177
|
+
if (name && BARE_KEY_RE.test(name)) return `:${name}`;
|
|
178
|
+
return quoteString(String(key));
|
|
179
|
+
}
|
|
180
|
+
if (BARE_KEY_RE.test(key)) return key;
|
|
181
|
+
return quoteString(key);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatValue(value, indent, depth) {
|
|
185
|
+
if (value === null) return 'null';
|
|
186
|
+
if (value === undefined) return 'undefined';
|
|
187
|
+
const t = typeof value;
|
|
188
|
+
if (t === 'number' || t === 'boolean') return String(value);
|
|
189
|
+
if (t === 'string') return quoteString(value);
|
|
190
|
+
if (t === 'symbol') {
|
|
191
|
+
const name = Symbol.keyFor(value);
|
|
192
|
+
if (name && BARE_KEY_RE.test(name)) return `:${name}`;
|
|
193
|
+
throw new StringifyError('cannot stringify non-interned symbol');
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
if (value.length === 0) return '[]';
|
|
197
|
+
const pad = indent.repeat(depth + 1);
|
|
198
|
+
const closePad = indent.repeat(depth);
|
|
199
|
+
const items = value.map((v) => formatValue(v, indent, depth + 1));
|
|
200
|
+
return `[\n${pad}${items.join(`\n${pad}`)}\n${closePad}]`;
|
|
201
|
+
}
|
|
202
|
+
if (t === 'object') {
|
|
203
|
+
// Reject class instances and exotic objects — they don't round-trip.
|
|
204
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
205
|
+
throw new StringifyError(`cannot stringify ${value.constructor?.name || 'non-plain'} instance`);
|
|
206
|
+
}
|
|
207
|
+
const keys = Object.keys(value);
|
|
208
|
+
if (keys.length === 0) return '{}';
|
|
209
|
+
const pad = indent.repeat(depth + 1);
|
|
210
|
+
const closePad = indent.repeat(depth);
|
|
211
|
+
const pairs = keys.map((k) => {
|
|
212
|
+
const v = value[k];
|
|
213
|
+
return `${formatKey(k)}: ${formatValue(v, indent, depth + 1)}`;
|
|
214
|
+
});
|
|
215
|
+
return `{\n${pad}${pairs.join(`\n${pad}`)}\n${closePad}}`;
|
|
216
|
+
}
|
|
217
|
+
throw new StringifyError(`cannot stringify value of type ${t}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function stringify(value, opts = {}) {
|
|
221
|
+
let indent = opts.indent ?? ' ';
|
|
222
|
+
if (typeof indent === 'number') indent = ' '.repeat(indent);
|
|
223
|
+
return formatValue(value, indent, 0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export { StringifyError };
|
|
227
|
+
|
|
228
|
+
// -----------------------------------------------------------------------------
|
|
229
|
+
// 3. TypeScript declarations for stdlib helpers
|
|
230
|
+
// -----------------------------------------------------------------------------
|
|
231
|
+
//
|
|
232
|
+
// `typecheck.js` injects these into the shadow `.d.ts` file it feeds the
|
|
233
|
+
// TypeScript language service so user code that calls `assert(x)`,
|
|
234
|
+
// `kind(v)`, etc. type-checks correctly. Helpers not listed here fall back
|
|
235
|
+
// to a generic `(...args: any[]) => any` signature in typecheck.js.
|
|
236
|
+
|
|
237
|
+
export const STDLIB_TYPE_DECLS = {
|
|
238
|
+
abort: 'declare function abort(msg?: string): never;',
|
|
239
|
+
assert: 'declare function assert(v: any, msg?: string): asserts v;',
|
|
240
|
+
exit: 'declare function exit(code?: number): never;',
|
|
241
|
+
kind: 'declare function kind(v: any): string;',
|
|
242
|
+
noop: 'declare function noop(): void;',
|
|
243
|
+
p: 'declare function p(...args: any[]): void;',
|
|
244
|
+
pp: 'declare function pp<T>(v: T): T;',
|
|
245
|
+
pr: 'declare function pr<T>(v: T): T;',
|
|
246
|
+
pj: 'declare function pj<T>(v: T): T;',
|
|
247
|
+
raise: 'declare function raise(a: any, b?: any): never;',
|
|
248
|
+
rand: 'declare function rand(a?: number, b?: number): number;',
|
|
249
|
+
sleep: 'declare function sleep(ms: number): Promise<void>;',
|
|
250
|
+
todo: 'declare function todo(msg?: string): never;',
|
|
251
|
+
warn: 'declare function warn(...args: any[]): void;',
|
|
252
|
+
zip: 'declare function zip(...arrays: any[][]): any[][];',
|
|
253
|
+
};
|