-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathupdate-code.js
More file actions
483 lines (438 loc) · 20.9 KB
/
Copy pathupdate-code.js
File metadata and controls
483 lines (438 loc) · 20.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
'use strict'
define((require) => {
let {parseLine,isLineStart} = require('parse-line')
let parseString = require('expression/parse-string')
let system = require('play/system')
let players = require('player/players')
let sections = require('section/sections')
let mainVars = require('main-vars')
let consoleOut = require('console')
let sliders = require('functions/sliders')
let persistentState = require('expression/persistent-state')
let predefinedVars = require('predefined-vars')
let vars = require('vars')
let mainBus = require('play/main-bus')
let parseLinesAndComments = (code) => {
let state = {
str: code,
idx: 0,
lastLineStart: 0,
inComment: false,
commentStart: -1,
inLineComment: false,
lineCommentStart: -1,
}
let lines = []
let char
while (true) {
char = state.str.charAt(state.idx)
if (char === '\n' || char === '') {
let lineStart = state.lastLineStart
let lineEnd = state.idx
if (state.inLineComment) { // // comment end
lineEnd = state.lineCommentStart
state.inLineComment = false
state.lineCommentStart = -1
}
if (state.inComment) {
if (state.commentStart !== -1) { // /* comment started on this line
lineEnd = state.commentStart
state.commentStart = -1
} else {
lineStart = state.idx // Entire line is in comment
}
}
let line = state.str.slice(lineStart, lineEnd).trim()+' '
lines.push(line)
state.idx += 1
state.lastLineStart = state.idx
if (char === '') { break }
} else if (char === '\'' && !state.inLineComment && !state.inComment) { // String - skip over
state.idx += 1
parseString(state)
} else if (char === '/' && state.str.charAt(state.idx+1) === '/' && !state.inLineComment && !state.inComment) { // // Comment start
state.lineCommentStart = state.idx
state.inLineComment = true
state.idx += 2
} else if (char === '/' && state.str.charAt(state.idx+1) === '*' && !state.inLineComment && !state.inComment) { // /* Comment start
state.commentStart = state.idx
state.inComment = true
state.idx += 2
} else if (state.inComment && char === '*' && state.str.charAt(state.idx+1) === '/') { // /* Comment end
state.idx += 2
state.inComment = false
if (state.commentStart !== -1) { // Comment started on this line
state.str = state.str.slice(0, state.commentStart) + state.str.slice(state.idx) // Trim out commented section
state.idx -= state.idx - state.commentStart
} else {
state.str = state.str.slice(0, state.lastLineStart) + state.str.slice(state.idx) // Trim out commented section on this line
state.idx -= state.idx - state.lastLineStart
}
state.commentStart = -1
} else {
state.idx += 1
}
}
return lines
}
let sectionBlockStartRegex = /^\s*section\s+([_a-zA-Z]\w*)\s*\{\s*$/i
let sectionBlockEndRegex = /^\s*\}/
let parseCommand = async (lines, i, url, parseCode) => {
let line = lines[i]
if (line === '') { return i }
if (line.startsWith('//') === '') { return i }
let blockMatch = line.match(sectionBlockStartRegex)
if (blockMatch) {
// Section block: accumulate raw lines up to the closing `}` line, preserving newlines so
// parse-line can treat each body line as its own command
let acc = [line]
let closed = false
while ((i+1)<lines.length) {
i++
acc.push(lines[i])
if (sectionBlockEndRegex.test(lines[i])) { closed = true; break }
}
if (!closed) { // Report and consume the lines; throwing would leave the body lines to be parsed as normal commands
consoleOut(`🔴 Parse error: Missing } to close section block '${blockMatch[1]}'`)
return i
}
while ((i+1)<lines.length && !isLineStart(lines[i+1])) { // Params after the closing `}` may span lines like any command
acc[acc.length-1] += lines[i+1]
i++
}
line = acc.join('\n')
} else {
let acc = [line]
while ((i+1)<lines.length && !isLineStart(lines[i+1])) {
acc.push(lines[i+1])
i++
}
line = acc.join('')
}
await parseLine(line, i, parseCode, undefined, url)
return i
}
// A code update can be superseded part way through - a second Ctrl+Enter, or a stop - while it is
// suspended on an include fetch. Each update takes a generation; once that moves on, the older parse
// abandons rather than carrying on writing into state that has since been torn down or replaced.
let updateGeneration = 0
let cancelUpdate = () => {
updateGeneration++
players.updating = false // Nothing left to wait for; don't leave continuous players held off forever
}
let makeParseCode = (gen) => {
let parse = async (code, url) => {
let lines
lines = parseLinesAndComments(code)
for (let i = 0; i<lines.length; i++) {
if (gen !== undefined && gen !== updateGeneration) { return } // Superseded by a newer update, or cancelled
try {
i = await parseCommand(lines, i, url, parse) // Will skip lines that were accumulated
} catch (e) {
consoleOut('🔴 Parse error: ' + e)
console.log(e)
}
}
}
return parse
}
let parseCode = makeParseCode(undefined) // Ungated; used for the startup preset loads and the tests
// Buses are the only continuous player type, and they were held off for the duration of the update.
// Start them here rather than leaving it to the next beat: a bus input is only wired through to its
// output inside start(), so anything routed into an unstarted bus is dropped.
let startContinuousPlayers = () => {
for (let id in players.instances) { // Insertion order, so the reserved buses (parsed first) start first
let player = players.instances[id]
if (!player || !player.startIfPending) { continue }
try {
player.startIfPending()
} catch (e) {
consoleOut('🔴 Run Error from player '+id+': ' + e)
console.log(e)
}
}
}
let latestCode
let updateCode = async (code, options = {}) => {
system.resume()
if (!options.auto) { latestCode = code } // Remember for automatic reruns on section change
let gen = ++updateGeneration // Supersedes any update still parsing, so it stops writing into this one's state
let parse = makeParseCode(gen)
players.updating = true // Hold continuous players off until every override in the code has been parsed
players.gc_reset()
sections.gc_reset()
sections.resetDefault() // Baseline default each update; a `section default` line then redefines it
mainVars.reset()
players.overrides = {}
sliders.gc_reset()
persistentState.gc_reset() // accum/smooth/rate state is kept across the re-parse, keyed per param
vars.clear()
predefinedVars.apply(vars.all())
if (options.auto) {
consoleOut(`> Section '${sections.active ? sections.active.name : '?'}': update code`)
} else {
consoleOut('> Update code')
}
sections.suppressForce = !!options.auto // set section.active/next lines must not refire on automatic reruns
try {
await parse(mainBus())
await parse(code)
} finally {
sections.suppressForce = false
// Safe to clear here even though the overrides are not final yet: everything from this point to
// startContinuousPlayers() below is synchronous, so no beat can land in between. Doing it in the
// finally means an unexpected throw can't leave every bus held off for good.
if (gen === updateGeneration) { players.updating = false }
}
if (gen !== updateGeneration) { return } // Superseded mid parse; whoever bumped the generation owns the state now
players.gc_sweep()
sections.gc_sweep()
sliders.gc_sweep()
persistentState.gc_sweep() // Anything whose line has gone away was not marked by this parse
players.expandOverrides()
// Route `set <name> ...` lines that name a section onto that section. After expandOverrides, so
// wildcards (which only ever match players) have already been resolved; and after gc_sweep, so
// only surviving sections are considered.
players.overrides = sections.extractOverrides(players.overrides, id => !!players.getById(id))
startContinuousPlayers() // Overrides are final; buses may latch them now
}
// Rerun the last code after the active section changed, so section-scoped lines
// (section blocks) take effect for the newly active section
let rerunForSectionChange = async () => {
if (latestCode === undefined) { return }
await updateCode(latestCode, {auto: true})
}
// TESTS //
if ((new URLSearchParams(window.location.search)).get('test') !== null) {
let vars = require('vars').all()
let assert = (expected, actual) => {
let x = JSON.stringify(expected, (k,v) => (typeof v == 'number') ? (v+0.0001).toFixed(2) : v)
let a = JSON.stringify(actual, (k,v) => (typeof v == 'number') ? (v+0.0001).toFixed(2) : v)
if (x !== a) { console.trace(`Assertion failed.\n>>Expected:\n ${x}\n>>Actual:\n ${a}`) }
}
let assertVars = async (code, expected) => {
await parseCode(code)
Object.keys(expected).forEach(k => {
assert(expected[k], vars[k])
delete vars[k]
})
}
let assertOverrides = async (code, playerId, expected) => {
await parseCode(code)
Object.keys(expected).forEach(k => {
assert(expected[k], players.overrides[playerId][k])
})
delete players.overrides[playerId]
}
let assertThrows = async (expected, code) => {
let got
try {await code()}
catch (e) { if (e.includes(expected)) {got=true} else {console.trace(`Assertion failed.\n>>Expected throw: ${expected}\n>>Actual: ${e}`)} }
finally { if (!got) console.trace(`Assertion failed.\n>>Expected throw: ${expected}\n>>Actual: none` ) }
}
assertVars('', {})
assertVars(' \n \t\n ', {})
assertVars('//set fooa=1+1', {fooa:undefined})
assertVars('set foob=1+1', {foob:2})
assertVars(' \n//yo \nset fooc=1+1 \n \n \n\n set barc = 2 + 2 \n ', {fooc:2,barc:4})
assertOverrides('set pa amp=2', 'pa', {amp:2})
assertOverrides('set pb foo=2, bar=4', 'pb', {foo:2,bar:4})
assertOverrides('set pc foo=2,\n,bar=4', 'pc', {foo:2,bar:4})
assertOverrides('set pd foo=2 , \n , bar=4', 'pd', {foo:2,bar:4})
assertOverrides('set pe \nfoo=2,bar=4', 'pe', {foo:2,bar:4})
assertOverrides('set pf foo\n=2,bar=4', 'pf', {foo:2,bar:4})
assertOverrides('set pg foo=\n2,bar=4', 'pg', {foo:2,bar:4})
assertOverrides('set ph foo=2\n,bar=4', 'ph', {foo:2,bar:4})
assertOverrides('set pi foo=2,\nbar=4', 'pi', {foo:2,bar:4})
assertOverrides('set pj foo=2,bar\n=4', 'pj', {foo:2,bar:4})
assertOverrides('set pk foo=2,bar=\n4', 'pk', {foo:2,bar:4})
assertOverrides(' set pl \n foo = 2 , bar = 4 ', 'pl', {foo:2,bar:4})
assertOverrides(' set pm foo \n = 2 , bar = 4 ', 'pm', {foo:2,bar:4})
assertOverrides(' set pn foo = \n 2 , bar = 4 ', 'pn', {foo:2,bar:4})
assertOverrides(' set po foo = 2 \n , bar = 4 ', 'po', {foo:2,bar:4})
assertOverrides(' set pp foo = 2 , \n bar = 4 ', 'pp', {foo:2,bar:4})
assertOverrides(' set pq foo = 2 , bar \n = 4 ', 'pq', {foo:2,bar:4})
assertOverrides(' set pr foo = 2 , bar = \n 4 ', 'pr', {foo:2,bar:4})
assertOverrides('set ps foo=2,\\\nbar=4', 'ps', {foo:2,bar:undefined})
assert(undefined, vars.bar)
assertOverrides('set pt foo=2, HELLO\nbar=4', 'pt', {foo:2,bar:undefined})
assert(undefined, vars.bar)
assertVars('set food=(\n1,\n2,\n3)', {food:[1,2,3]})
assertVars('set fooe=(\n1,\n//2,\n3)', {fooe:[1,3]})
assertVars("set foof='http://a.com/Bc.mp3'", {foof:'http://a.com/Bc.mp3'})
assertVars("set foog='http://a.com/B\\c.mp3'", {foog:'http://a.com/Bc.mp3'})
assertVars("set fooh='http://a.com/Bc.mp3'//FOO", {fooh:'http://a.com/Bc.mp3'})
assertOverrides("set pu foo=//'http://a.com/Bc.mp3'", 'pu', {foo:undefined})
assertVars('set fooi=\n 1', {fooi:1})
assertVars('set fooj= \n 1', {fooj:1})
assertVars('set fook= //Cmnt \n 1', {fook:1})
assertVars('set fool= //Cmnt\n 1', {fool:1})
assertVars("set foom= //Cm'nt\n 1", {foom:1})
assertOverrides("set pv //s='abc'", 'pv', {})
assertOverrides("set pw s//='abc'", 'pw', {s:1})
assertOverrides("set px s=//'abc'", 'px', {})
assertOverrides("set py a=1//,s='abc'", 'py', {a:1})
assertOverrides("set pz a=1//1,s='abc'", 'pz', {a:1})
assertOverrides("set paa a=1,//s='abc'", 'paa', {a:1})
assertOverrides("set pab a=1, //s='abc'", 'pab', {a:1})
assertOverrides("set pac str='http://', amp=0.1, rate=10", 'pac', {str:'http://', amp:0.1, rate:10})
assertOverrides("set pad window//, rate=2", 'pad', {window:1})
assertOverrides("set pae add//+=2", 'pae', {add:1})
assertOverrides("set paf add+//=2", 'paf', {'add+':1})
assertOverrides("set pca s=1, t=2", 'pca', {s:1,t:2})
assertOverrides("set pcb /*s=1,*/ t=2", 'pcb', {t:2})
assertOverrides("set pcc /* s=1 */, t=2", 'pcc', {t:2})
assertOverrides("set pcd/* s=1*/, t=2", 'pcd', {t:2})
assertOverrides("set pce s/*='abc'*/, ", 'pce', {s:1})
assertOverrides("set pcf s='abc/*def*/'", 'pcf', {s:'abc/*def*/'})
assertOverrides("set pcg s='abc/*def'", 'pcg', {s:'abc/*def'})
assertOverrides("set pch s='abc*/def'", 'pch', {s:'abc*/def'})
assertOverrides("set pci s=1//, /*t=2*/", 'pci', {s:1})
assertOverrides("set pcj s=1, ///*t=2*/", 'pcj', {s:1})
assertOverrides("set pck s=1, //*t=2*/", 'pck', {s:1})
assertOverrides("set pcl s=1, /*t//=2*/", 'pcl', {s:1})
assertOverrides("set pcm add=1/*+2*/+3\n//+5", 'pcm', {add:4})
assertOverrides("set pmca \n s=1, \n t=2", 'pmca', {s:1,t:2})
assertOverrides("set pmcb /* \n s=1, \n */ t=2", 'pmcb', {t:2})
// Sections and section blocks. All async tests that touch the shared sections state
// (active, instances, hasBlocks) must be sequenced in this one IIFE — separate async
// test blocks interleave at the awaits and clobber each other's section state.
;(async () => {
let savedActive = sections.active
// Sections are swept on code update if no longer present
await parseCode('section sca, a=1')
assert(1, sections.instances.sca.a)
sections.gc_reset()
await parseCode('section scb')
sections.gc_sweep()
assert(undefined, sections.instances.sca)
assert('scb', sections.instances.scb.name)
delete sections.instances.scb
// Inactive section: block params parse, body lines skipped, hasBlocks flagged
sections.active = undefined
sections.hasBlocks = false
await parseCode('section sba {\nset sbax=1+1\n}, length=16, bar=3')
assert(16, sections.instances.sba.length)
assert(3, sections.instances.sba.bar)
assert(true, sections.hasBlocks)
assert(undefined, vars.sbax)
// Active section: body lines parsed
sections.active = sections.instances.sba
await parseCode('section sba {\nset sbax=1+1\n}, length=16')
assert(2, vars.sbax)
delete vars.sbax
// No params after the closing brace
await parseCode('section sba {\nset sbaw=5\n}')
assert(5, vars.sbaw)
assert(32, sections.instances.sba.length)
delete vars.sbaw
// Comments and continuations inside the body; params after } may span lines
await parseCode('section sba {\nset sbay=( //cmt\n1,\n2)\n}, length=8,\nfoo=3')
assert([1,2], vars.sbay)
assert(8, sections.instances.sba.length)
assert(3, sections.instances.sba.foo)
delete vars.sbay
// Player overrides in the body
await parseCode('section sba {\nset sbap amp=2\n}')
assert(2, players.overrides.sbap && players.overrides.sbap.amp)
delete players.overrides.sbap
// Multiple body commands
await parseCode('section sba {\nset sbad=1\nset sbae=2\n}')
assert(1, vars.sbad)
assert(2, vars.sbae)
delete vars.sbad
delete vars.sbae
// next param after the closing brace stays a raw name
await parseCode('section sba {\n}, next=sbb')
assert('sbb', sections.instances.sba.nextName)
// Nested section definitions are not allowed (parseLine throws; parseCode would swallow it)
await assertThrows('inside a section block', () => parseLine('section sba {\nsection sbb, length=8\n}'))
// The built-in default section matches by name. (Restore default first: the gc tests above
// call gc_sweep directly without resetDefault; real updateCode calls resetDefault so it survives.)
sections.resetDefault()
sections.active = sections.default
await parseCode('section default {\nset sbdf=7\n}')
assert(7, vars.sbdf)
delete vars.sbdf
// The default section can be redefined (length + body); resetDefault reverts to baseline
sections.active = sections.instances.default
await parseCode('section default {\nset dfx=1\n}, length=8')
assert(8, sections.instances.default.length)
assert(1, vars.dfx)
delete vars.dfx
sections.resetDefault() // What updateCode runs each update; removing the block reverts default
assert(8, sections.instances.default.length)
// Unterminated block is a parse error; nothing is defined and the body lines don't leak out as commands
sections.active = undefined
let errored = false
let realLog = console.log
let consEl = document.getElementById('console')
let savedConsVal = consEl.value // Suppress the expected parse error output in the on-page console too
console.log = () => { errored = true } // Suppress the expected parse error output
await parseCode('section sbz {\nset sbzz=9')
console.log = realLog
consEl.value = savedConsVal
assert(true, errored)
assert(undefined, sections.instances.sbz)
assert(undefined, vars.sbzz)
delete sections.instances.sba
// Section-scoped overrides swap when the code is re-parsed after a section change
let code = 'section sca2 {\nset scaa amp=1\n}, next=scb2\nsection scb2 {\nset scaa amp=2\n}'
sections.active = undefined
await parseCode(code)
assert('scb2', sections.instances.sca2.nextName)
assert(undefined, players.overrides.scaa) // Neither section active; no overrides applied
sections.active = sections.instances.sca2
await parseCode(code)
assert(1, players.overrides.scaa && players.overrides.scaa.amp)
sections.active = sections.instances.scb2
delete players.overrides.scaa // Cleared by updateCode in real use
await parseCode(code)
assert(2, players.overrides.scaa && players.overrides.scaa.amp)
delete players.overrides.scaa
delete sections.instances.sca2
delete sections.instances.scb2
// `set <name> param=value` overrides a section of that name (what updateCode's
// extractOverrides call does after parsing; here on just the keys these lines produce)
let route = (id) => sections.extractOverrides({ [id]: players.overrides[id] }, i => !!players.getById(i))
sections.overrides = {}
await parseCode('section sov, length=8\nset sov length=4, foo=3')
assert(4, players.overrides.sov && players.overrides.sov.length) // parses as a player override...
assert({}, route('sov')) // ...and routes to the section
assert(4, sections.getLength(sections.instances.sov, 0))
assert(3, sections.getParam(sections.instances.sov, 'foo'))
delete players.overrides.sov
// A player of the same name wins; the section is left alone
sections.overrides = {}
players.instances.sov = { currentEvent: () => [] }
await parseCode('section sov, length=8\nset sov length=4')
assert(4, route('sov').sov.length) // stays with the player
assert(8, sections.getLength(sections.instances.sov, 0))
delete players.instances.sov
delete players.overrides.sov
delete sections.instances.sov
// The built-in default section can be overridden, including compounding with its default length
sections.resetDefault()
sections.overrides = {}
await parseCode('set default length=4')
route('default')
assert(4, sections.getLength(sections.default, 0))
delete players.overrides.default
sections.overrides = {}
await parseCode('set default length+=4')
route('default')
assert(12, sections.getLength(sections.default, 0))
delete players.overrides.default
sections.overrides = {}
sections.active = savedActive
sections.hasBlocks = false
})().catch(e => console.trace('Assertion failed.\n>>Section block test error: ' + e))
console.log('Update code tests complete')
}
return {
parseCode:parseCode,
updateCode:updateCode,
cancelUpdate:cancelUpdate,
rerunForSectionChange:rerunForSectionChange,
}
})