Monorepo for Aesthetic.Computer aesthetic.computer
at main 827 lines 22 kB view raw view rendered
1# KidLisp Optimization Implementation Guide 2 3**Generated:** August 17, 2025 4**Analyst:** GitHub Copilot 5**Target:** Optimizing KidLisp based on TinyLisp architecture insights 6**Implementation:** JavaScript/ES6 with backward compatibility 7**Performance Goal:** 3-10x improvement while maintaining feature set 8 9--- 10 11## Immediate Wins (Phase 1) 12 13### 1. Bytecode Compilation System 14 15Replace the current AST evaluation with a bytecode compilation approach: 16 17```javascript 18// New bytecode operations 19const OPCODES = { 20 LOAD_NUM: 0, 21 LOAD_VAR: 1, 22 LOAD_FUNC: 2, 23 CALL: 3, 24 RETURN: 4, 25 JUMP_IF_FALSE: 5, 26 JUMP: 6, 27 POP: 7, 28 DUP: 8 29}; 30 31class KidLispBytecode { 32 constructor() { 33 this.instructions = []; 34 this.constants = []; 35 this.stack = []; 36 this.pc = 0; // program counter 37 } 38 39 compile(ast) { 40 this.instructions = []; 41 this.constants = []; 42 this.compileExpression(ast); 43 return this; 44 } 45 46 compileExpression(expr) { 47 if (typeof expr === 'number') { 48 const constIndex = this.addConstant(expr); 49 this.emit(OPCODES.LOAD_NUM, constIndex); 50 } else if (typeof expr === 'string') { 51 this.emit(OPCODES.LOAD_VAR, this.addConstant(expr)); 52 } else if (Array.isArray(expr)) { 53 const [head, ...args] = expr; 54 55 // Compile arguments first (reverse order for stack) 56 for (let i = args.length - 1; i >= 0; i--) { 57 this.compileExpression(args[i]); 58 } 59 60 // Load function 61 this.emit(OPCODES.LOAD_FUNC, this.addConstant(head)); 62 this.emit(OPCODES.CALL, args.length); 63 } 64 } 65 66 addConstant(value) { 67 const index = this.constants.indexOf(value); 68 if (index !== -1) return index; 69 this.constants.push(value); 70 return this.constants.length - 1; 71 } 72 73 emit(opcode, operand = 0) { 74 this.instructions.push({ opcode, operand }); 75 } 76 77 execute(api, globalEnv) { 78 this.stack = []; 79 this.pc = 0; 80 81 while (this.pc < this.instructions.length) { 82 const { opcode, operand } = this.instructions[this.pc]; 83 84 switch (opcode) { 85 case OPCODES.LOAD_NUM: 86 this.stack.push(this.constants[operand]); 87 break; 88 89 case OPCODES.LOAD_VAR: 90 const varName = this.constants[operand]; 91 const value = globalEnv[varName] || this.localEnv[varName]; 92 this.stack.push(value); 93 break; 94 95 case OPCODES.LOAD_FUNC: 96 const funcName = this.constants[operand]; 97 this.stack.push(globalEnv[funcName]); 98 break; 99 100 case OPCODES.CALL: 101 const argCount = operand; 102 const func = this.stack.pop(); 103 const args = []; 104 for (let i = 0; i < argCount; i++) { 105 args.unshift(this.stack.pop()); 106 } 107 const result = func(api, args); 108 this.stack.push(result); 109 break; 110 } 111 this.pc++; 112 } 113 114 return this.stack.pop(); 115 } 116} 117 118// Integration with existing KidLisp class 119class OptimizedKidLisp extends KidLisp { 120 constructor() { 121 super(); 122 this.compiledBytecode = null; 123 this.bytecodeCache = new Map(); 124 } 125 126 parse(input) { 127 const ast = super.parse(input); 128 129 // Check cache first 130 const cacheKey = JSON.stringify(ast); 131 if (this.bytecodeCache.has(cacheKey)) { 132 this.compiledBytecode = this.bytecodeCache.get(cacheKey); 133 } else { 134 // Compile to bytecode 135 this.compiledBytecode = new KidLispBytecode().compile(ast); 136 this.bytecodeCache.set(cacheKey, this.compiledBytecode); 137 } 138 139 return ast; 140 } 141 142 evaluate(expr, api, env) { 143 if (this.compiledBytecode) { 144 return this.compiledBytecode.execute(api, this.getGlobalEnv()); 145 } 146 return super.evaluate(expr, api, env); 147 } 148} 149``` 150 151### 2. Function Memoization for Pure Functions 152 153```javascript 154class MemoizedFunction { 155 constructor(func, isPure = true, maxCacheSize = 1000) { 156 this.func = func; 157 this.isPure = isPure; 158 this.cache = new Map(); 159 this.maxCacheSize = maxCacheSize; 160 this.hitCount = 0; 161 this.missCount = 0; 162 } 163 164 call(api, args) { 165 if (!this.isPure) { 166 // Non-pure functions always execute 167 return this.func(api, args); 168 } 169 170 const key = this.hashArgs(args); 171 172 if (this.cache.has(key)) { 173 this.hitCount++; 174 return this.cache.get(key); 175 } 176 177 this.missCount++; 178 const result = this.func(api, args); 179 180 // LRU eviction 181 if (this.cache.size >= this.maxCacheSize) { 182 const firstKey = this.cache.keys().next().value; 183 this.cache.delete(firstKey); 184 } 185 186 this.cache.set(key, result); 187 return result; 188 } 189 190 hashArgs(args) { 191 // Simple hash for memoization key 192 return JSON.stringify(args); 193 } 194 195 getStats() { 196 const total = this.hitCount + this.missCount; 197 return { 198 hitCount: this.hitCount, 199 missCount: this.missCount, 200 hitRate: total > 0 ? (this.hitCount / total * 100).toFixed(1) + '%' : '0%' 201 }; 202 } 203} 204 205// Mark functions as pure for memoization 206const PURE_FUNCTIONS = new Set([ 207 '+', '-', '*', '/', '%', 'mod', 208 '=', '>', '<', '>=', '<=', 209 'abs', 'sqrt', 'sin', 'cos', 'tan', 210 'floor', 'ceil', 'round', 211 'min', 'max' 212]); 213 214// Enhanced global environment with memoization 215class OptimizedKidLisp extends KidLisp { 216 getGlobalEnv() { 217 if (this.globalEnvCache) { 218 return this.globalEnvCache; 219 } 220 221 const baseEnv = super.getGlobalEnv(); 222 const optimizedEnv = {}; 223 224 // Wrap pure functions with memoization 225 for (const [name, func] of Object.entries(baseEnv)) { 226 if (typeof func === 'function' && PURE_FUNCTIONS.has(name)) { 227 optimizedEnv[name] = new MemoizedFunction(func, true).call.bind( 228 new MemoizedFunction(func, true) 229 ); 230 } else { 231 optimizedEnv[name] = func; 232 } 233 } 234 235 this.globalEnvCache = optimizedEnv; 236 return optimizedEnv; 237 } 238} 239``` 240 241### 3. Fast Environment Lookup 242 243```javascript 244class FastEnvironment { 245 constructor() { 246 this.scopes = [new Map()]; // Stack of scope maps 247 this.localVarStack = []; // For faster local variable access 248 } 249 250 pushScope() { 251 this.scopes.push(new Map()); 252 this.localVarStack.push({}); 253 } 254 255 popScope() { 256 this.scopes.pop(); 257 this.localVarStack.pop(); 258 } 259 260 define(name, value) { 261 const currentScope = this.scopes[this.scopes.length - 1]; 262 currentScope.set(name, value); 263 264 // Also add to local var stack for O(1) access 265 const currentLocals = this.localVarStack[this.localVarStack.length - 1]; 266 currentLocals[name] = value; 267 } 268 269 lookup(name) { 270 // Check local var stack first (fastest) 271 for (let i = this.localVarStack.length - 1; i >= 0; i--) { 272 if (this.localVarStack[i].hasOwnProperty(name)) { 273 return this.localVarStack[i][name]; 274 } 275 } 276 277 // Fallback to scope chain 278 for (let i = this.scopes.length - 1; i >= 0; i--) { 279 if (this.scopes[i].has(name)) { 280 return this.scopes[i].get(name); 281 } 282 } 283 284 return undefined; 285 } 286 287 update(name, value) { 288 // Update in the scope where it was first defined 289 for (let i = this.scopes.length - 1; i >= 0; i--) { 290 if (this.scopes[i].has(name)) { 291 this.scopes[i].set(name, value); 292 // Also update local var stack 293 for (let j = this.localVarStack.length - 1; j >= 0; j--) { 294 if (this.localVarStack[j].hasOwnProperty(name)) { 295 this.localVarStack[j][name] = value; 296 break; 297 } 298 } 299 return true; 300 } 301 } 302 return false; 303 } 304} 305 306// Integration with KidLisp 307class OptimizedKidLisp extends KidLisp { 308 constructor() { 309 super(); 310 this.fastEnv = new FastEnvironment(); 311 } 312 313 evaluate(expr, api, env) { 314 // Use fast environment for variable lookups 315 if (typeof expr === 'string') { 316 const value = this.fastEnv.lookup(expr); 317 if (value !== undefined) { 318 return value; 319 } 320 // Fallback to global environment 321 const globalEnv = this.getGlobalEnv(); 322 return globalEnv[expr]; 323 } 324 325 return super.evaluate(expr, api, env); 326 } 327} 328``` 329 330## Medium-Term Optimizations (Phase 2) 331 332### 4. Optimized Timing Expression Engine 333 334```javascript 335class TimingEngine { 336 constructor() { 337 this.timers = new Map(); 338 this.sequenceStates = new Map(); 339 this.compiledTimingExpressions = new Map(); 340 } 341 342 compileTimingExpression(timingToken, expressions) { 343 const cacheKey = timingToken + JSON.stringify(expressions); 344 345 if (this.compiledTimingExpressions.has(cacheKey)) { 346 return this.compiledTimingExpressions.get(cacheKey); 347 } 348 349 let interval, isRepeating, isInstant; 350 351 if (/^\d*\.?\d+s\.\.\.?$/.test(timingToken)) { 352 // Repeating timer like "1s..." 353 interval = parseFloat(timingToken.slice(0, -4)) * 1000; // Convert to ms 354 isRepeating = true; 355 isInstant = false; 356 } else if (/^\d*\.?\d+s!?$/.test(timingToken)) { 357 // Delay timer like "1s" or instant "1s!" 358 interval = parseFloat(timingToken.replace(/[s!]/g, '')) * 1000; 359 isRepeating = false; 360 isInstant = timingToken.endsWith('!'); 361 } 362 363 const compiled = { 364 interval, 365 isRepeating, 366 isInstant, 367 expressions: expressions.slice(), // Copy array 368 lastExecutionTime: 0, 369 currentIndex: 0, 370 hasExecuted: false 371 }; 372 373 this.compiledTimingExpressions.set(cacheKey, compiled); 374 return compiled; 375 } 376 377 executeTiming(timingToken, expressions, api, currentTime) { 378 const compiled = this.compileTimingExpression(timingToken, expressions); 379 const timeSinceLastExecution = currentTime - compiled.lastExecutionTime; 380 381 // Handle instant triggers 382 if (compiled.isInstant && !compiled.hasExecuted) { 383 compiled.hasExecuted = true; 384 compiled.lastExecutionTime = currentTime; 385 return this.executeExpressions(compiled.expressions, api); 386 } 387 388 // Handle interval-based execution 389 if (timeSinceLastExecution >= compiled.interval) { 390 compiled.lastExecutionTime = currentTime; 391 392 if (compiled.isRepeating) { 393 // Cycle through expressions 394 const result = this.executeExpressions([compiled.expressions[compiled.currentIndex]], api); 395 compiled.currentIndex = (compiled.currentIndex + 1) % compiled.expressions.length; 396 return result; 397 } else { 398 // Execute all expressions once 399 return this.executeExpressions(compiled.expressions, api); 400 } 401 } 402 403 // Return current state for repeating expressions 404 if (compiled.isRepeating && compiled.expressions.length > 0) { 405 return compiled.expressions[compiled.currentIndex]; 406 } 407 408 return null; 409 } 410 411 executeExpressions(expressions, api) { 412 let result = null; 413 for (const expr of expressions) { 414 result = this.evaluateExpression(expr, api); 415 } 416 return result; 417 } 418 419 evaluateExpression(expr, api) { 420 // This would delegate back to the main evaluator 421 // but with optimized context 422 return expr; // Simplified for example 423 } 424} 425 426// Integration with main evaluator 427class OptimizedKidLisp extends KidLisp { 428 constructor() { 429 super(); 430 this.timingEngine = new TimingEngine(); 431 } 432 433 evaluate(expr, api, env) { 434 if (Array.isArray(expr)) { 435 const [head, ...args] = expr; 436 437 // Fast path for timing expressions 438 if (typeof head === 'string' && /^\d*\.?\d+s/.test(head)) { 439 const currentTime = Date.now(); 440 return this.timingEngine.executeTiming(head, args, api, currentTime); 441 } 442 } 443 444 return super.evaluate(expr, api, env); 445 } 446} 447``` 448 449### 5. Symbol Interning for Reduced String Allocation 450 451```javascript 452class SymbolTable { 453 constructor() { 454 this.symbols = new Map(); // string -> symbol id 455 this.symbolNames = []; // symbol id -> string 456 this.nextId = 0; 457 } 458 459 intern(name) { 460 if (this.symbols.has(name)) { 461 return this.symbols.get(name); 462 } 463 464 const id = this.nextId++; 465 this.symbols.set(name, id); 466 this.symbolNames[id] = name; 467 return id; 468 } 469 470 getName(id) { 471 return this.symbolNames[id]; 472 } 473 474 getSymbol(name) { 475 return this.symbols.get(name); 476 } 477 478 size() { 479 return this.nextId; 480 } 481} 482 483// Use interned symbols for function names and variables 484class OptimizedKidLisp extends KidLisp { 485 constructor() { 486 super(); 487 this.symbolTable = new SymbolTable(); 488 this.internedFunctions = new Map(); 489 } 490 491 parse(input) { 492 const ast = super.parse(input); 493 return this.internSymbols(ast); 494 } 495 496 internSymbols(expr) { 497 if (typeof expr === 'string' && /^[a-zA-Z_]/.test(expr)) { 498 // Intern function names and variables 499 return this.symbolTable.intern(expr); 500 } else if (Array.isArray(expr)) { 501 return expr.map(item => this.internSymbols(item)); 502 } 503 return expr; 504 } 505 506 evaluate(expr, api, env) { 507 // Handle interned symbols 508 if (typeof expr === 'number' && expr < this.symbolTable.size()) { 509 const symbolName = this.symbolTable.getName(expr); 510 if (symbolName) { 511 const globalEnv = this.getGlobalEnv(); 512 return globalEnv[symbolName]; 513 } 514 } 515 516 return super.evaluate(expr, api, env); 517 } 518} 519``` 520 521## Advanced Optimizations (Phase 3) 522 523### 6. Tagged Union Value System (JavaScript NaN Boxing Alternative) 524 525```javascript 526// Value type constants 527const VALUE_TYPES = { 528 NUMBER: 0, 529 STRING: 1, 530 FUNCTION: 2, 531 LIST: 3, 532 BOOLEAN: 4, 533 NIL: 5 534}; 535 536class KidLispValue { 537 constructor(type, data) { 538 this.type = type; 539 this.data = data; 540 } 541 542 static number(n) { 543 return new KidLispValue(VALUE_TYPES.NUMBER, n); 544 } 545 546 static string(s) { 547 return new KidLispValue(VALUE_TYPES.STRING, s); 548 } 549 550 static func(f) { 551 return new KidLispValue(VALUE_TYPES.FUNCTION, f); 552 } 553 554 static list(items) { 555 return new KidLispValue(VALUE_TYPES.LIST, items); 556 } 557 558 static bool(b) { 559 return new KidLispValue(VALUE_TYPES.BOOLEAN, b); 560 } 561 562 static nil() { 563 return new KidLispValue(VALUE_TYPES.NIL, null); 564 } 565 566 isNumber() { return this.type === VALUE_TYPES.NUMBER; } 567 isString() { return this.type === VALUE_TYPES.STRING; } 568 isFunction() { return this.type === VALUE_TYPES.FUNCTION; } 569 isList() { return this.type === VALUE_TYPES.LIST; } 570 isBoolean() { return this.type === VALUE_TYPES.BOOLEAN; } 571 isNil() { return this.type === VALUE_TYPES.NIL; } 572 573 getValue() { return this.data; } 574 575 toString() { 576 switch (this.type) { 577 case VALUE_TYPES.NUMBER: return this.data.toString(); 578 case VALUE_TYPES.STRING: return this.data; 579 case VALUE_TYPES.FUNCTION: return '[Function]'; 580 case VALUE_TYPES.LIST: return `(${this.data.map(v => v.toString()).join(' ')})`; 581 case VALUE_TYPES.BOOLEAN: return this.data ? '#t' : '#f'; 582 case VALUE_TYPES.NIL: return '()'; 583 default: return '[Unknown]'; 584 } 585 } 586} 587 588// Memory pool for value objects to reduce GC pressure 589class ValuePool { 590 constructor(initialSize = 1000) { 591 this.pool = []; 592 this.index = 0; 593 594 // Pre-allocate value objects 595 for (let i = 0; i < initialSize; i++) { 596 this.pool.push(new KidLispValue(VALUE_TYPES.NIL, null)); 597 } 598 } 599 600 getValue(type, data) { 601 if (this.index >= this.pool.length) { 602 // Pool exhausted, create new value 603 return new KidLispValue(type, data); 604 } 605 606 const value = this.pool[this.index++]; 607 value.type = type; 608 value.data = data; 609 return value; 610 } 611 612 reset() { 613 this.index = 0; 614 } 615} 616``` 617 618### 7. JIT Compilation for Hot Paths 619 620```javascript 621class JITCompiler { 622 constructor() { 623 this.hotPaths = new Map(); 624 this.executionCounts = new Map(); 625 this.compiledFunctions = new Map(); 626 this.JIT_THRESHOLD = 100; // Compile after 100 executions 627 } 628 629 trackExecution(expression) { 630 const key = JSON.stringify(expression); 631 const count = this.executionCounts.get(key) || 0; 632 this.executionCounts.set(key, count + 1); 633 634 if (count + 1 >= this.JIT_THRESHOLD && !this.compiledFunctions.has(key)) { 635 this.compileHotPath(key, expression); 636 } 637 } 638 639 compileHotPath(key, expression) { 640 try { 641 // Generate optimized JavaScript code for hot mathematical expressions 642 if (this.isMathExpression(expression)) { 643 const compiledFunction = this.compileMathExpression(expression); 644 this.compiledFunctions.set(key, compiledFunction); 645 console.log(`📊 JIT compiled hot path: ${key}`); 646 } 647 } catch (error) { 648 console.warn(`⚠️ JIT compilation failed for ${key}:`, error); 649 } 650 } 651 652 isMathExpression(expr) { 653 if (!Array.isArray(expr)) return false; 654 const [head] = expr; 655 return ['+', '-', '*', '/', '%', 'sin', 'cos', 'tan', 'sqrt'].includes(head); 656 } 657 658 compileMathExpression(expr) { 659 const jsCode = this.generateJavaScript(expr); 660 return new Function('args', `return ${jsCode}`); 661 } 662 663 generateJavaScript(expr) { 664 if (typeof expr === 'number') { 665 return expr.toString(); 666 } 667 668 if (typeof expr === 'string') { 669 // Variable reference 670 return `args['${expr}']`; 671 } 672 673 if (Array.isArray(expr)) { 674 const [head, ...args] = expr; 675 676 switch (head) { 677 case '+': 678 return args.map(arg => this.generateJavaScript(arg)).join(' + '); 679 case '-': 680 if (args.length === 1) { 681 return `-${this.generateJavaScript(args[0])}`; 682 } 683 return args.map(arg => this.generateJavaScript(arg)).join(' - '); 684 case '*': 685 return args.map(arg => this.generateJavaScript(arg)).join(' * '); 686 case '/': 687 return args.map(arg => this.generateJavaScript(arg)).join(' / '); 688 case '%': 689 return args.map(arg => this.generateJavaScript(arg)).join(' % '); 690 case 'sin': 691 return `Math.sin(${this.generateJavaScript(args[0])})`; 692 case 'cos': 693 return `Math.cos(${this.generateJavaScript(args[0])})`; 694 case 'tan': 695 return `Math.tan(${this.generateJavaScript(args[0])})`; 696 case 'sqrt': 697 return `Math.sqrt(${this.generateJavaScript(args[0])})`; 698 default: 699 throw new Error(`Unsupported operation: ${head}`); 700 } 701 } 702 703 throw new Error(`Cannot compile expression: ${expr}`); 704 } 705 706 executeOrCompile(expression, context) { 707 const key = JSON.stringify(expression); 708 this.trackExecution(expression); 709 710 if (this.compiledFunctions.has(key)) { 711 // Use compiled version 712 return this.compiledFunctions.get(key)(context); 713 } 714 715 // Fall back to interpreter 716 return null; // Indicates should use interpreter 717 } 718} 719 720// Integration with main evaluator 721class OptimizedKidLisp extends KidLisp { 722 constructor() { 723 super(); 724 this.jitCompiler = new JITCompiler(); 725 } 726 727 evaluate(expr, api, env) { 728 // Try JIT compilation for hot mathematical expressions 729 if (Array.isArray(expr)) { 730 const jitResult = this.jitCompiler.executeOrCompile(expr, { ...this.globalDef, ...this.localEnv }); 731 if (jitResult !== null) { 732 return jitResult; 733 } 734 } 735 736 return super.evaluate(expr, api, env); 737 } 738} 739``` 740 741## Benchmarking Implementation 742 743```javascript 744class KidLispBenchmark { 745 constructor() { 746 this.tests = { 747 'arithmetic': ['(+ 1 2 3 4 5)', '(* (+ 2 3) (- 4 1))', '(/ (* 7 8) (+ 2 3))'], 748 'function-calls': ['(line 0 0 100 100)', '(ink "red")', '(box 10 10 50 50)'], 749 'timing': ['(1s (ink "red"))', '(0.5s... "red" "blue" "green")'], 750 'variables': ['(def x 10) (+ x x x)', '(def y (+ 1 2 3)) (* y y)'], 751 'complex': ['(repeat 10 (+ (* x 2) (/ y 3)))', '(if (> x 5) (+ x 10) (- x 5))'] 752 }; 753 } 754 755 run(implementations, iterations = 1000) { 756 const results = {}; 757 758 for (const [name, implementation] of Object.entries(implementations)) { 759 console.log(`\n🏃 Benchmarking ${name}...`); 760 results[name] = {}; 761 762 for (const [testName, testCases] of Object.entries(this.tests)) { 763 const totalTime = this.benchmarkTestCase(implementation, testCases, iterations); 764 results[name][testName] = { 765 totalTime: totalTime.toFixed(2) + 'ms', 766 avgTime: (totalTime / iterations).toFixed(4) + 'ms', 767 opsPerSec: Math.round(iterations / (totalTime / 1000)) 768 }; 769 } 770 } 771 772 this.printResults(results); 773 return results; 774 } 775 776 benchmarkTestCase(implementation, testCases, iterations) { 777 const start = performance.now(); 778 779 for (let i = 0; i < iterations; i++) { 780 for (const testCase of testCases) { 781 try { 782 const ast = implementation.parse(testCase); 783 implementation.evaluate(ast, {}, {}); 784 } catch (e) { 785 // Ignore errors for benchmarking 786 } 787 } 788 } 789 790 return performance.now() - start; 791 } 792 793 printResults(results) { 794 console.log('\n📊 Benchmark Results:'); 795 console.log('='.repeat(80)); 796 797 const implementations = Object.keys(results); 798 const testNames = Object.keys(results[implementations[0]]); 799 800 // Print header 801 console.log('Test'.padEnd(15) + implementations.map(impl => impl.padEnd(20)).join('')); 802 console.log('-'.repeat(80)); 803 804 // Print results 805 for (const testName of testNames) { 806 let row = testName.padEnd(15); 807 for (const impl of implementations) { 808 const result = results[impl][testName]; 809 row += `${result.avgTime} (${result.opsPerSec}/s)`.padEnd(20); 810 } 811 console.log(row); 812 } 813 } 814} 815 816// Usage example 817const benchmark = new KidLispBenchmark(); 818const implementations = { 819 'Original': new KidLisp(), 820 'Optimized': new OptimizedKidLisp() 821}; 822 823// Run benchmarks 824benchmark.run(implementations); 825``` 826 827This implementation guide provides concrete code examples for each optimization phase, allowing for incremental improvement of KidLisp's performance while maintaining compatibility and extensibility.