runtime.mjs raw

   1  // TinyJS Runtime — Core
   2  // Provides panic/recover/defer, program initialization, and console I/O.
   3  
   4  export class GoPanic extends Error {
   5    constructor(value) {
   6      super(typeof value === 'string' ? value : JSON.stringify(value));
   7      this.goValue = value;
   8      this.name = 'GoPanic';
   9    }
  10  }
  11  
  12  // Defer stack. Each goroutine has its own.
  13  export class DeferStack {
  14    constructor() {
  15      this.frames = [];
  16    }
  17  
  18    push(fn) {
  19      this.frames.push(fn);
  20    }
  21  
  22    run() {
  23      let panicked = null;
  24      while (this.frames.length > 0) {
  25        const fn = this.frames.pop();
  26        try {
  27          fn();
  28        } catch (e) {
  29          panicked = e;
  30        }
  31      }
  32      if (panicked) throw panicked;
  33    }
  34  }
  35  
  36  // Recover returns the panic value if called inside a deferred function
  37  // during a panic. Otherwise returns null.
  38  let currentPanicValue = null;
  39  let recovering = false;
  40  
  41  export function setPanic(val) {
  42    currentPanicValue = val;
  43  }
  44  
  45  export function recover() {
  46    if (recovering) {
  47      const val = currentPanicValue;
  48      currentPanicValue = null;
  49      recovering = false;
  50      return val;
  51    }
  52    return null;
  53  }
  54  
  55  // Panic halts the current goroutine. Deferred functions run first.
  56  export function panic(value) {
  57    throw new GoPanic(value);
  58  }
  59  
  60  // Run deferred functions, handling panic/recover.
  61  export async function runDefers(deferStack) {
  62    const frames = [...deferStack.frames].reverse();
  63    deferStack.frames = [];
  64    let lastPanic = null;
  65  
  66    for (const fn of frames) {
  67      try {
  68        recovering = lastPanic !== null;
  69        if (recovering) {
  70          currentPanicValue = lastPanic instanceof GoPanic ? lastPanic.goValue : lastPanic;
  71        }
  72        const result = fn();
  73        if (result instanceof Promise) await result;
  74        if (recovering && currentPanicValue === null) {
  75          lastPanic = null; // recovered
  76        }
  77        recovering = false;
  78      } catch (e) {
  79        recovering = false;
  80        lastPanic = e;
  81      }
  82    }
  83  
  84    if (lastPanic) throw lastPanic;
  85  }
  86  
  87  // Synchronous version of defer stack runner for generated code.
  88  // Takes an array of deferred functions (already in push order; will pop from end).
  89  export function runDeferStack(defers, panicValue) {
  90    let lastPanic = panicValue || null;
  91    while (defers.length) {
  92      const fn = defers.pop();
  93      try {
  94        recovering = lastPanic !== null;
  95        if (recovering) {
  96          currentPanicValue = lastPanic instanceof GoPanic ? lastPanic.goValue : lastPanic;
  97        }
  98        fn();
  99        // If recover() was called, it clears currentPanicValue to null.
 100        // Check if the panic was recovered (currentPanicValue cleared by recover()).
 101        if (currentPanicValue === null && lastPanic !== null) {
 102          lastPanic = null; // recovered
 103        }
 104        recovering = false;
 105      } catch (e) {
 106        recovering = false;
 107        lastPanic = e;
 108      }
 109    }
 110    currentPanicValue = null;
 111    recovering = false;
 112    if (lastPanic) throw lastPanic;
 113  }
 114  
 115  // Zero values for Moxie types.
 116  export function zeroValue(typeId) {
 117    switch (typeId) {
 118      case 'bool': return false;
 119      case 'int64': case 'uint64':
 120        return 0n;
 121      case 'int': case 'int8': case 'int16': case 'int32':
 122      case 'uint': case 'uint8': case 'uint16': case 'uint32':
 123      case 'float32': case 'float64':
 124        return 0;
 125      default:
 126        return null;
 127    }
 128  }
 129  
 130  // Println — maps to fmt.Println for the runtime.
 131  export function println(...args) {
 132    console.log(args.map(a => {
 133      if (typeof a === 'string') return a;
 134      if (typeof a === 'boolean') return a ? 'true' : 'false';
 135      if (a === null || a === undefined) return '<nil>';
 136      return String(a);
 137    }).join(' '));
 138  }
 139  
 140  // Print without newline.
 141  export function print(...args) {
 142    const text = args.map(String).join(' ');
 143    console.log(text);
 144  }
 145  
 146  // Program exit.
 147  export function exit(code) {
 148    throw new Error('exit: ' + code);
 149  }
 150  
 151  // Nil check helper.
 152  export function nilCheck(ptr, op) {
 153    if (ptr === null || ptr === undefined) {
 154      panic('runtime error: invalid memory address or nil pointer dereference');
 155    }
 156  }
 157  
 158  // Bounds check helper.
 159  export function boundsCheck(index, length) {
 160    if (index < 0 || index >= length) {
 161      panic(`runtime error: index out of range [${index}] with length ${length}`);
 162    }
 163  }
 164  
 165  // Slice bounds check.
 166  export function sliceBoundsCheck(low, high, max) {
 167    if (low < 0 || high < low || max < high) {
 168      panic(`runtime error: slice bounds out of range [${low}:${high}:${max}]`);
 169    }
 170  }
 171