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 Go types.
 116  export function zeroValue(typeId) {
 117    switch (typeId) {
 118      case 'bool': return false;
 119      case 'int': case 'int8': case 'int16': case 'int32': case 'int64':
 120      case 'uint': case 'uint8': case 'uint16': case 'uint32': case 'uint64':
 121      case 'float32': case 'float64':
 122      case 'uintptr':
 123        return 0;
 124      case 'complex64': case 'complex128':
 125        return { re: 0, im: 0 };
 126      case 'string':
 127        return '';
 128      default:
 129        return null;
 130    }
 131  }
 132  
 133  // Println — maps to fmt.Println for the runtime.
 134  export function println(...args) {
 135    console.log(args.map(a => {
 136      if (typeof a === 'string') return a;
 137      if (typeof a === 'boolean') return a ? 'true' : 'false';
 138      if (a === null || a === undefined) return '<nil>';
 139      return String(a);
 140    }).join(' '));
 141  }
 142  
 143  // Print without newline.
 144  export function print(...args) {
 145    const text = args.map(String).join(' ');
 146    if (typeof process !== 'undefined' && process.stdout) {
 147      process.stdout.write(text);
 148    } else {
 149      console.log(text);
 150    }
 151  }
 152  
 153  // Program exit.
 154  export function exit(code) {
 155    if (typeof process !== 'undefined' && process.exit) {
 156      process.exit(code);
 157    } else {
 158      throw new Error('exit: ' + code);
 159    }
 160  }
 161  
 162  // Nil check helper.
 163  export function nilCheck(ptr, op) {
 164    if (ptr === null || ptr === undefined) {
 165      panic('runtime error: invalid memory address or nil pointer dereference');
 166    }
 167  }
 168  
 169  // Bounds check helper.
 170  export function boundsCheck(index, length) {
 171    if (index < 0 || index >= length) {
 172      panic(`runtime error: index out of range [${index}] with length ${length}`);
 173    }
 174  }
 175  
 176  // Slice bounds check.
 177  export function sliceBoundsCheck(low, high, max) {
 178    if (low < 0 || high < low || max < high) {
 179      panic(`runtime error: slice bounds out of range [${low}:${high}:${max}]`);
 180    }
 181  }
 182