rockstar-strudel 1.0.7 → 1.0.10
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 +63 -0
- package/package.json +1 -1
- package/src/index.js +62 -16
- package/test/rockstar.test.js +125 -1
package/README.md
CHANGED
|
@@ -75,6 +75,39 @@ const shifted = await melody.rerun(62)
|
|
|
75
75
|
const shiftedAgain = await shifted.rerun(([prevRoot]) => [prevRoot + 2])
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
### Using `to_base()` for melodic number conversion
|
|
79
|
+
|
|
80
|
+
Convert output numbers to different bases and feed them into a Strudel sequence:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
const { init, rockstar, rockstar_pro, to_base } = await import('https://esm.sh/rockstar-strudel')
|
|
84
|
+
|
|
85
|
+
// Pre-warm the WASM engine while other code loads (optional but recommended)
|
|
86
|
+
await init()
|
|
87
|
+
|
|
88
|
+
// Run a Rockstar program
|
|
89
|
+
const prog = await rockstar_pro`
|
|
90
|
+
Papa was a rolling stone
|
|
91
|
+
(Wherever he laid his hat)
|
|
92
|
+
(was his home)
|
|
93
|
+
(And when he died)
|
|
94
|
+
(All he left us was alone)
|
|
95
|
+
Say Papa
|
|
96
|
+
Build up Papa
|
|
97
|
+
Scream Papa
|
|
98
|
+
`
|
|
99
|
+
|
|
100
|
+
samples('shabda/speech:'+prog.speech.join(','))
|
|
101
|
+
|
|
102
|
+
$: s(slowcat(prog.speech.slice(0,5)).degradeBy(.9))
|
|
103
|
+
|
|
104
|
+
$: note(seq(to_base(prog.output, 10).flat(2))
|
|
105
|
+
.slow("<4 2 4>")
|
|
106
|
+
.scale("C:major")
|
|
107
|
+
)
|
|
108
|
+
.sound("piano").every(4, rev)
|
|
109
|
+
```
|
|
110
|
+
|
|
78
111
|
|
|
79
112
|
### Template interpolations
|
|
80
113
|
|
|
@@ -109,6 +142,8 @@ Tagged-template function with richer parallel output views:
|
|
|
109
142
|
- `output`: numeric-first values (`number` or nested numeric arrays).
|
|
110
143
|
- `mixed_output`: mixed typed values with words preserved.
|
|
111
144
|
- `text_output`: fully stringified values for text/speech use.
|
|
145
|
+
- `error`: `null` on success, otherwise `{ name, message }` describing a runtime
|
|
146
|
+
failure from the underlying runner.
|
|
112
147
|
- `speech`: sanitized line tokens derived from source code for Shabda speech
|
|
113
148
|
sample lookup (for example `samples('shabda/speech:'+prog.speech.join(','))`).
|
|
114
149
|
- `templateValues`: the interpolation values used for this run.
|
|
@@ -121,6 +156,24 @@ emitted line.
|
|
|
121
156
|
For JSON-style list output (for example `[ "012" ]`), all views parse the same
|
|
122
157
|
structure, and `output` resolves that case to `[12]`.
|
|
123
158
|
|
|
159
|
+
When a runtime error occurs, `rockstar_pro` returns the same object shape with
|
|
160
|
+
`output`, `mixed_output`, and `text_output` left as `[]`, while `error` carries
|
|
161
|
+
the failure details.
|
|
162
|
+
|
|
163
|
+
Example error handling:
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
const prog = await rockstar_pro`
|
|
167
|
+
pri nt "Hello, World"
|
|
168
|
+
`
|
|
169
|
+
|
|
170
|
+
if (prog.error) {
|
|
171
|
+
console.error(`Rockstar runtime error: ${prog.error.message}`)
|
|
172
|
+
} else {
|
|
173
|
+
note(seq(prog.output)).sound("piano")
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
124
177
|
### `init([dotnetUrl])` → `Promise<void>`
|
|
125
178
|
|
|
126
179
|
Pre-loads the WASM engine. Optionally accepts a custom `dotnet.js` URL (see
|
|
@@ -148,6 +201,16 @@ an ellipsis (`...` or `…`) introduces the decimal separator.
|
|
|
148
201
|
Parses one raw callback line into a dual-view structure used by
|
|
149
202
|
`rockstar_pro` (`raw`, `output`, `poetic`). Exported for testing.
|
|
150
203
|
|
|
204
|
+
### `to_base(number, base)` → `array | array[]`
|
|
205
|
+
|
|
206
|
+
Converts a number or array of numbers to the specified base (2–36), returning
|
|
207
|
+
an array of digits or an array of digit arrays. Useful for converting Rockstar
|
|
208
|
+
output into melodic sequences via Strudel's `seq()`.
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
- `to_base(1344, 10)` → `[1, 3, 4, 4]`
|
|
212
|
+
- `to_base([1344, 23], 16)` → `[[5, 4, 0], [1, 7]]`
|
|
213
|
+
|
|
151
214
|
---
|
|
152
215
|
|
|
153
216
|
## How it works
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rockstar-strudel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Run Rockstar lang programs via the Starship WASM engine, returning output as a JS array. Designed for use in the strudel.cc live-coding REPL.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/index.js
CHANGED
|
@@ -69,6 +69,27 @@ export function isTrustedUrl(url) {
|
|
|
69
69
|
*/
|
|
70
70
|
let _runnerPromise = null;
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Test-only seam: replace the cached runner promise.
|
|
74
|
+
* This allows unit tests to validate `rockstar` / `rockstar_pro`
|
|
75
|
+
* behavior without loading the WASM runtime.
|
|
76
|
+
*
|
|
77
|
+
* @param {Promise<object> | null} runnerPromise
|
|
78
|
+
* @returns {void}
|
|
79
|
+
*/
|
|
80
|
+
export function __setRunnerPromiseForTests(runnerPromise) {
|
|
81
|
+
_runnerPromise = runnerPromise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Test-only helper: clear the cached runner promise.
|
|
86
|
+
*
|
|
87
|
+
* @returns {void}
|
|
88
|
+
*/
|
|
89
|
+
export function __resetRunnerForTests() {
|
|
90
|
+
_runnerPromise = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
/**
|
|
73
94
|
* Pre-load the Rockstar WASM engine.
|
|
74
95
|
*
|
|
@@ -496,6 +517,7 @@ export async function rockstar_pro(strings, ...values) {
|
|
|
496
517
|
const output = [];
|
|
497
518
|
const mixed_output = [];
|
|
498
519
|
const text_output = [];
|
|
520
|
+
let error = null;
|
|
499
521
|
const lines = code.split('\n')
|
|
500
522
|
const speech = lines.map((x) => x.toLowerCase()
|
|
501
523
|
.trim()
|
|
@@ -503,23 +525,29 @@ export async function rockstar_pro(strings, ...values) {
|
|
|
503
525
|
.replace(/\W/g, ''))
|
|
504
526
|
.filter((x)=> x.length);
|
|
505
527
|
|
|
506
|
-
console.log(`samples('shabda/speech:'+prog.speech.join(','))`)
|
|
507
|
-
|
|
508
|
-
await runner.Run(
|
|
509
|
-
code,
|
|
510
|
-
(line) => {
|
|
511
|
-
raw_output.push(line);
|
|
528
|
+
//console.log(`samples('shabda/speech:'+prog.speech.join(','))`)
|
|
512
529
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
530
|
+
try {
|
|
531
|
+
await runner.Run(
|
|
532
|
+
code,
|
|
533
|
+
(line) => {
|
|
534
|
+
raw_output.push(line);
|
|
535
|
+
|
|
536
|
+
const parsed = parseOutputLine(line);
|
|
537
|
+
if (!parsed) return;
|
|
538
|
+
|
|
539
|
+
output.push(parsed.output);
|
|
540
|
+
mixed_output.push(parsed.mixed_output);
|
|
541
|
+
text_output.push(parsed.text_output);
|
|
542
|
+
},
|
|
543
|
+
/* stdin */ '',
|
|
544
|
+
/* args */ ''
|
|
545
|
+
);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
error = err instanceof Error
|
|
548
|
+
? { name: err.name, message: err.message }
|
|
549
|
+
: { name: 'Error', message: String(err) };
|
|
550
|
+
}
|
|
523
551
|
|
|
524
552
|
const unsupported = (featureName) => {
|
|
525
553
|
throw new Error(
|
|
@@ -535,6 +563,7 @@ export async function rockstar_pro(strings, ...values) {
|
|
|
535
563
|
output,
|
|
536
564
|
mixed_output,
|
|
537
565
|
text_output,
|
|
566
|
+
error,
|
|
538
567
|
speech,
|
|
539
568
|
rerun: (...nextArgs) => {
|
|
540
569
|
const nextValues = resolveRerunValues(values, ...nextArgs);
|
|
@@ -545,3 +574,20 @@ export async function rockstar_pro(strings, ...values) {
|
|
|
545
574
|
listFunctions: () => unsupported('listFunctions()'),
|
|
546
575
|
};
|
|
547
576
|
}
|
|
577
|
+
|
|
578
|
+
export const to_base = function (number, base) {
|
|
579
|
+
const convertOne = function (value) {
|
|
580
|
+
let digit = [];
|
|
581
|
+
while (value > 0) {
|
|
582
|
+
digit.unshift(value % base);
|
|
583
|
+
value = Math.floor(value / base);
|
|
584
|
+
}
|
|
585
|
+
return digit;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
if (Array.isArray(number)) {
|
|
589
|
+
return number.map(convertOne);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return convertOne(number);
|
|
593
|
+
}
|
package/test/rockstar.test.js
CHANGED
|
@@ -7,14 +7,18 @@
|
|
|
7
7
|
* Run with: npm test
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, it } from 'node:test';
|
|
10
|
+
import { beforeEach, describe, it } from 'node:test';
|
|
11
11
|
import assert from 'node:assert/strict';
|
|
12
12
|
import {
|
|
13
|
+
__resetRunnerForTests,
|
|
14
|
+
__setRunnerPromiseForTests,
|
|
13
15
|
buildSource,
|
|
14
16
|
coerce,
|
|
15
17
|
isTrustedUrl,
|
|
16
18
|
parsePoeticNumber,
|
|
17
19
|
parseOutputLine,
|
|
20
|
+
rockstar,
|
|
21
|
+
rockstar_pro,
|
|
18
22
|
resolveRerunValues,
|
|
19
23
|
} from '../src/index.js';
|
|
20
24
|
|
|
@@ -286,3 +290,123 @@ describe('resolveRerunValues', () => {
|
|
|
286
290
|
assert.deepEqual(next, [99]);
|
|
287
291
|
});
|
|
288
292
|
});
|
|
293
|
+
|
|
294
|
+
// ─── example-program output shaping (non-WASM) ────────────────────────────
|
|
295
|
+
|
|
296
|
+
describe('example Rockstar program outputs (non-WASM shaping)', () => {
|
|
297
|
+
const collectViews = (rawLines) => {
|
|
298
|
+
const output = [];
|
|
299
|
+
const mixed_output = [];
|
|
300
|
+
const text_output = [];
|
|
301
|
+
|
|
302
|
+
for (const line of rawLines) {
|
|
303
|
+
const parsed = parseOutputLine(line);
|
|
304
|
+
if (!parsed) continue;
|
|
305
|
+
output.push(parsed.output);
|
|
306
|
+
mixed_output.push(parsed.mixed_output);
|
|
307
|
+
text_output.push(parsed.text_output);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { output, mixed_output, text_output };
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
it('matches the two-line numeric output shape from the Papa + Scream example', () => {
|
|
314
|
+
const parsed = collectViews(['175\n', '179\n']);
|
|
315
|
+
assert.deepEqual(parsed.output, [175, 179]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('matches rockstar numeric output for a single Say line', () => {
|
|
319
|
+
const parsed = collectViews(['175\n']);
|
|
320
|
+
assert.deepEqual(parsed.output, [175]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('matches rockstar_pro numeric output for a single Say line', () => {
|
|
324
|
+
const parsed = collectViews(['175\n']);
|
|
325
|
+
assert.deepEqual(parsed.output, [175]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('matches successive numeric updates from the He was with him example', () => {
|
|
329
|
+
const parsed = collectViews(['175\n', '350\n']);
|
|
330
|
+
assert.deepEqual(parsed.output, [175, 350]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('keeps AC/DC-like output text in mixed/text views', () => {
|
|
334
|
+
const parsed = collectViews(['AD/DC\n']);
|
|
335
|
+
assert.deepEqual(parsed.mixed_output, ['AD/DC']);
|
|
336
|
+
assert.deepEqual(parsed.text_output, ['AD/DC']);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('produces empty parallel views when no lines are emitted', () => {
|
|
340
|
+
const parsed = collectViews([]);
|
|
341
|
+
assert.deepEqual(parsed.output, []);
|
|
342
|
+
assert.deepEqual(parsed.mixed_output, []);
|
|
343
|
+
assert.deepEqual(parsed.text_output, []);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ─── rockstar / rockstar_pro wrappers via mocked runner ───────────────────
|
|
348
|
+
|
|
349
|
+
describe('rockstar wrappers (mocked runner)', () => {
|
|
350
|
+
beforeEach(() => {
|
|
351
|
+
__resetRunnerForTests();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('rockstar returns numeric-first output values', async () => {
|
|
355
|
+
const fakeRunner = {
|
|
356
|
+
async Run(_code, onLine) {
|
|
357
|
+
onLine('175\n');
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
__setRunnerPromiseForTests(Promise.resolve(fakeRunner));
|
|
361
|
+
|
|
362
|
+
const out = await rockstar`
|
|
363
|
+
Papa was a rolling stone
|
|
364
|
+
Say Papa
|
|
365
|
+
`;
|
|
366
|
+
|
|
367
|
+
assert.deepEqual(out, [175]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('rockstar_pro returns parallel output views', async () => {
|
|
371
|
+
const fakeRunner = {
|
|
372
|
+
async Run(_code, onLine) {
|
|
373
|
+
onLine('175\n');
|
|
374
|
+
onLine('350\n');
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
__setRunnerPromiseForTests(Promise.resolve(fakeRunner));
|
|
378
|
+
|
|
379
|
+
const out = await rockstar_pro`
|
|
380
|
+
Papa was a rolling stone
|
|
381
|
+
Say Papa
|
|
382
|
+
He was with him
|
|
383
|
+
Say Papa
|
|
384
|
+
`;
|
|
385
|
+
|
|
386
|
+
assert.deepEqual(out.output, [175, 350]);
|
|
387
|
+
assert.deepEqual(out.mixed_output, [175, 350]);
|
|
388
|
+
assert.deepEqual(out.text_output, ['175', '350']);
|
|
389
|
+
assert.equal(out.error, null);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('rockstar_pro preserves [] outputs and exposes an error field on runtime failure', async () => {
|
|
393
|
+
const fakeRunner = {
|
|
394
|
+
async Run() {
|
|
395
|
+
throw new Error('Parse error near "pri nt"');
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
__setRunnerPromiseForTests(Promise.resolve(fakeRunner));
|
|
399
|
+
|
|
400
|
+
const out = await rockstar_pro`
|
|
401
|
+
pri nt "Hello, World"
|
|
402
|
+
`;
|
|
403
|
+
|
|
404
|
+
assert.deepEqual(out.output, []);
|
|
405
|
+
assert.deepEqual(out.mixed_output, []);
|
|
406
|
+
assert.deepEqual(out.text_output, []);
|
|
407
|
+
assert.deepEqual(out.error, {
|
|
408
|
+
name: 'Error',
|
|
409
|
+
message: 'Parse error near "pri nt"',
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
});
|