Skip to content

Instantly share code, notes, and snippets.

@shanewholloway
Last active July 21, 2021 23:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shanewholloway/1d1179efd28c58953629e069b72a2687 to your computer and use it in GitHub Desktop.
Save shanewholloway/1d1179efd28c58953629e069b72a2687 to your computer and use it in GitHub Desktop.

Rollup behavior change regression

I've extracted a class import issue I'm seeing in unit tests into this smaller reproduction.

  • rollup v2.48.0 works correctly, as with the previous v2.39 dependency
  • rollup v2.49.0 to v2.52.7 errors with RangeError: Maximum call stack size exceeded
  • rollup v2.52.8 to v2.53.3 runs out of memory and core dumps.

Gist contents

git clone https://gist.github.com/shanewholloway/1d1179efd28c58953629e069b72a2687
cd 1d1179efd28c58953629e069b72a2687
  • ./package.json has a rollup dependency and entrypoints that call rollup -c
  • ./unittest.mjs is a short ESM snippet that triggers the issue
  • ./rollup.config.js is a bare-bones rollup config

The ./cbor_codec_decode.mjs comes from my [cbor-codec](https://github.com/shanewholloway/js-cbor-codec] project, specifically cbor-codec@0.8.0/esm/decode.mjs and is itself an ESM module compiled using Rollup. Should be "normal"-ish.

Reproduce

git clone https://gist.github.com/shanewholloway/1d1179efd28c58953629e069b72a2687
cd 1d1179efd28c58953629e069b72a2687

npm install
npm test

Change the rollup depency to 2.48.0 to see it work, 2.52.7 to see recursion failure, and 2.53.3 to see memory core dumpe failure.

class CBORDecoderBase {
// Possible monkeypatch apis responsibilities:
// decode() ::
// *iter_decode() ::
// async decode_stream() ::
// async * aiter_decode_stream() ::
static options(options) {
return (class extends this {})
.compile(options)}
static compile(options) {
this.prototype.compile(options);
return this}
constructor(options) {
if (null != options) {
this.compile(options);}
this._U8Ctx_.bind_decode_api(this);}
compile(options) {
this.jmp = this._bind_cbor_jmp(options, this.jmp);
if (options.types) {
this.types = Object.assign(
Object.create(this.types || null),
options.types); }
this._U8Ctx_ = this._bind_u8ctx(
this.types, this.jmp, options.unknown);
return this} }
Array.from(Array(256),
(_, v) => v.toString(2).padStart(8, '0'));
Array.from(Array(256),
(_, v) => v.toString(16).padStart(2, '0'));
function u8_to_utf8(u8) {
return new TextDecoder('utf-8').decode(u8) }
function as_u8_buffer(u8) {
if (u8 instanceof Uint8Array) {
return u8}
if (ArrayBuffer.isView(u8)) {
return new Uint8Array(u8.buffer)}
if (u8 instanceof ArrayBuffer) {
return new Uint8Array(u8)}
return Uint8Array.from(u8)}
function u8_concat(parts) {
let i=0, len=0;
for (const b of parts) {
const byteLength = b.byteLength;
if ('number' !== typeof byteLength) {
throw new Error("Invalid part byteLength") }
len += byteLength;}
const u8 = new Uint8Array(len);
for (const u8_part of parts) {
u8.set(u8_part, i);
i += u8_part.byteLength;}
return u8}
const cbor_decode_sym = Symbol('CBOR-decode');
const cbor_encode_sym = Symbol('CBOR-encode');
const cbor_break_sym = Symbol('CBOR-break');
const cbor_done_sym = Symbol('CBOR-done');
const cbor_eoc_sym = Symbol('CBOR-EOC');
const cbor_tagged_proto ={
[Symbol.toStringTag]: 'cbor_tag',
[cbor_encode_sym](enc_ctx, v) {
enc_ctx.tag_encode(v.tag, v.body);} };
function cbor_accum(base) {
return iv => ({
__proto__: base,
res: base.init(iv) })}
const decode_types ={
__proto__: null
, nested_cbor(u8, ctx) {
ctx = ctx.from_nested_u8(u8);
u8.decode_cbor = () => ctx.decode_cbor();
return u8}
, u32(u8, idx) {
const u32 = (u8[idx] << 24) | (u8[idx+1] << 16) | (u8[idx+2] << 8) | u8[idx+3];
return u32 >>> 0 }// unsigned int32
, u64(u8, idx) {
const v_hi = (u8[idx] << 24) | (u8[idx+1] << 16) | (u8[idx+2] << 8) | u8[idx+3];
const v_lo = (u8[idx+4] << 24) | (u8[idx+5] << 16) | (u8[idx+6] << 8) | u8[idx+7];
const u64 = (v_lo >>> 0) + 0x100000000*(v_hi >>> 0);
return u64}
, float16(u8) {
return {'@f2': u8}}
, float32(u8, idx=u8.byteOffset) {
return new DataView(u8.buffer, idx, 4).getFloat32(0)}
, float64(u8, idx=u8.byteOffset) {
return new DataView(u8.buffer, idx, 8).getFloat64(0)}
, bytes(u8) {return u8}
, bytes_stream:
cbor_accum({
init: () => []
, accum: _res_push
, done: res => u8_concat(res)})
, utf8(u8) {return u8_to_utf8(u8)}
, utf8_stream:
cbor_accum({
init: () => []
, accum: _res_push
, done: res => res.join('')})
, list:
cbor_accum({
init: () => []
, accum: _res_attr})
, list_stream() {
return this.list()}
, map:
cbor_accum({
init: () => ({})
, accum: _res_attr})
, map_stream() {
return this.map()} };
function _res_push(res,i,v) {res.push(v);}
function _res_attr(res,k,v) {res[k] = v;}
const decode_Map ={
map:
cbor_accum({
init: () => new Map()
, accum: (res, k, v) => res.set(k, v)}) };
const decode_Set ={
list:
cbor_accum({
init: () => new Set()
, accum: (res, i, v) => res.add(v)}) };
function basic_tags(tags_lut) {
// from https://tools.ietf.org/html/rfc7049#section-2.4
// Standard date/time string; see Section 2.4.1
tags_lut.set(0, () => ts_sz => new Date(ts_sz));
// Epoch-based date/time; see Section 2.4.1
tags_lut.set(1, () => seconds => new Date(seconds * 1000));
// Positive bignum; see Section 2.4.2
// tags_lut.set @ 2, () => v => v
// Negative bignum; see Section 2.4.2
// tags_lut.set @ 3, () => v => v
// Decimal fraction; see Section 2.4.3
// tags_lut.set @ 4, () => v => v
// Bigfloat; see Section 2.4.3
// tags_lut.set @ 5, () => v => v
// Expected conversion to base64url encoding; see Section 2.4.4.2
// tags_lut.set @ 21, () => v => v
// Expected conversion to base64 encoding; see Section 2.4.4.2
// tags_lut.set @ 22, () => v => v
// Expected conversion to base16 encoding; see Section 2.4.4.2
// tags_lut.set @ 23, () => v => v
// Encoded CBOR data item; see Section 2.4.4.1
tags_lut.set(24, ctx => u8 => ctx.types.nested_cbor(u8, ctx));
// URI; see Section 2.4.4.3
tags_lut.set(32, () => url_sz => new URL(url_sz));
// base64url; see Section 2.4.4.3
//tags_lut.set @ 33, () => v => v
// base64; see Section 2.4.4.3
//tags_lut.set @ 34, () => v => v
// Regular expression; see Section 2.4.4.3
//tags_lut.set @ 35, () => v => v
// MIME message; see Section 2.4.4.3
//tags_lut.set @ 36, () => v => v
// Self-describe CBOR; see Section 2.4.5
tags_lut.set(55799, () => {});
// EXTENSIONS
// CBOR Sets https://github.com/input-output-hk/cbor-sets-spec/blob/master/CBOR_SETS.md
tags_lut.set(258, ctx => { ctx.use_overlay(decode_Set); });
// CBOR Maps https://github.com/shanewholloway/js-cbor-codec/blob/master/docs/CBOR-256-spec--explicit-maps.md
tags_lut.set(259, ctx => { ctx.use_overlay(decode_Map); });
return tags_lut}
class U8DecodeBaseCtx {
static subclass(types, jmp, unknown) {
class U8DecodeCtx_ extends this {}
let {prototype} = U8DecodeCtx_;
prototype.next_value = U8DecodeCtx_.bind_next_value(jmp, unknown);
prototype.types = types;
return U8DecodeCtx_}
from_nested_u8(u8) {
return this.constructor
.from_u8(u8, this.types)}
use_overlay(overlay_types) {
let {types, _apply_overlay, _overlay_noop} = this;
if (_overlay_noop === _apply_overlay) {
_apply_overlay = () => {
this.types = types;}; }
this._apply_overlay = (() => {
this._apply_overlay = _apply_overlay;
this.types = overlay_types;} );
return types}
_error_unknown(ctx, type_b) {
throw new Error(`No CBOR decorder regeistered for ${type_b} (0x${('0'+type_b.toString(16)).slice(-2)})`) }
_overlay_noop() {}
// Subclass responsibilities:
// static bind_decode_api(decoder)
// static bind_next_value(jmp, unknown) ::
// move(count_bytes) ::
// Possible Subclass responsibilities:
// decode_cbor() ::
// *iter_decode_cbor() ::
// async decode_cbor() ::
}// async * aiter_decode_cbor() ::
class U8SyncDecodeCtx extends U8DecodeBaseCtx {
static bind_decode_api(decoder) {
decoder.decode = u8 =>
this.from_u8(u8, decoder.types)
.decode_cbor();
decoder.iter_decode = u8 =>
this.from_u8(u8, decoder.types)
.iter_decode_cbor();}
static get from_u8() {
const inst0 = new this();
return (u8, types) => {
u8 = as_u8_buffer(u8);
const inst ={
__proto__: inst0
, idx: 0, u8
, _apply_overlay: inst0._overlay_noop};
if (types && types !== inst0.types) {
inst.types = types;}
return inst} }
static bind_next_value(jmp, unknown) {
if (null == unknown) {
unknown = this._error_unknown;}
return function next_value() {
const doneTypes = this._apply_overlay();
const type_b = this.u8[ this.idx ++ ];
if (undefined === type_b) {
this.idx--;
throw cbor_done_sym}
const decode = jmp[type_b] || unknown;
const res = decode(this, type_b);
return undefined === doneTypes
? res : doneTypes(res)} }
decode_cbor() {
try {
return this.next_value()}
catch (e) {
throw cbor_done_sym !== e ? e
: new Error(`End of content`) } }
*iter_decode_cbor() {
try {
while (1) {
yield this.next_value();} }
catch (e) {
if (cbor_done_sym !== e) {
throw e} } }
move(count_bytes) {
const {idx, byteLength} = this;
const idx_next = idx + count_bytes;
if (idx_next >= byteLength) {
throw cbor_eoc_sym}
this.idx = idx_next;
return idx} }
const _cbor_jmp_base ={
bind_jmp(options, jmp) {
jmp = jmp ? jmp.slice()
: this.bind_basics_dispatch( new Map() );
if (null == options) {
options = {};}
if (options.simple) {
this.bind_jmp_simple(options, jmp);}
if (options.tags) {
this.bind_jmp_tag(options, jmp);}
return jmp}
, bind_jmp_simple(options, jmp) {
if (options.simple) {
const as_simple_value = this.bind_simple_dispatch(options.simple);
const tiny_simple = this.cbor_tiny(as_simple_value);
for (let i=0xe0; i<= 0xf3; i++) {
jmp[i] = tiny_simple;}
jmp[0xf8] = this.cbor_w1(as_simple_value);}
return jmp}
, bind_jmp_tag(options, jmp) {
if (options.tags) {
const as_tag = this.bind_tag_dispatch(
this.build_tags_lut(options.tags));
const tiny_tag = this.cbor_tiny(as_tag);
for (let i=0xc0; i<= 0xd7; i++) {
jmp[0xc0 | i] = tiny_tag;}
jmp[0xd8] = this.cbor_w1(as_tag);
jmp[0xd9] = this.cbor_w2(as_tag);
jmp[0xda] = this.cbor_w4(as_tag);
jmp[0xdb] = this.cbor_w8(as_tag);}
return jmp}
, bind_basics_dispatch(tags_lut) {
this.bind_tag_dispatch(tags_lut);
const tiny_pos_int = this.cbor_tiny(this.as_pos_int);
const tiny_neg_int = this.cbor_tiny(this.as_neg_int);
const tiny_bytes = this.cbor_tiny(this.as_bytes);
const tiny_utf8 = this.cbor_tiny(this.as_utf8);
const tiny_list = this.cbor_tiny(this.as_list);
const tiny_map = this.cbor_tiny(this.as_map);
const tiny_tag = this.cbor_tiny(this.as_tag);
const tiny_simple_repr = this.cbor_tiny(this.as_simple_repr);
const jmp = new Array(256);
for (let i=0; i<= 23; i++) {
jmp[0x00 | i] = tiny_pos_int;
jmp[0x20 | i] = tiny_neg_int;
jmp[0x40 | i] = tiny_bytes;
jmp[0x60 | i] = tiny_utf8;
jmp[0x80 | i] = tiny_list;
jmp[0xa0 | i] = tiny_map;
jmp[0xc0 | i] = tiny_tag;
jmp[0xe0 | i] = tiny_simple_repr;}
const cbor_widths =[
this.cbor_w1,
this.cbor_w2,
this.cbor_w4,
this.cbor_w8];
for (let w=0; w< 4; w++) {
const i = 24+w, cbor_wN = cbor_widths[w];
jmp[0x00 | i] = cbor_wN(this.as_pos_int);
jmp[0x20 | i] = cbor_wN(this.as_neg_int);
jmp[0x40 | i] = cbor_wN(this.as_bytes);
jmp[0x60 | i] = cbor_wN(this.as_utf8);
jmp[0x80 | i] = cbor_wN(this.as_list);
jmp[0xa0 | i] = cbor_wN(this.as_map);
jmp[0xc0 | i] = cbor_wN(this.as_tag);}
// streaming data types
jmp[0x5f] = ctx => this.as_stream(ctx, ctx.types.bytes_stream());
jmp[0x7f] = ctx => this.as_stream(ctx, ctx.types.utf8_stream());
jmp[0x9f] = ctx => this.as_stream(ctx, ctx.types.list_stream());
jmp[0xbf] = ctx => this.as_pair_stream(ctx, ctx.types.map_stream());
// semantic tag
// primitives
jmp[0xf4] = () => false;
jmp[0xf5] = () => true;
jmp[0xf6] = () => null;
jmp[0xf7] = () => {}; // undefined
jmp[0xf8] = this.cbor_w1(this.as_simple_repr);
jmp[0xf9] = this.as_float16;
jmp[0xfa] = this.as_float32;
jmp[0xfb] = this.as_float64;
//jmp[0xfc] = undefined
//jmp[0xfd] = undefined
//jmp[0xfe] = undefined
jmp[0xff] = () => cbor_break_sym;
return jmp}
, // simple values
as_pos_int: (ctx, value) => value,
as_neg_int: (ctx, value) => -1 - value,
as_simple_repr: (ctx, key) => `simple(${key})`,
bind_simple_dispatch(simple_lut) {
if ('function' !== typeof simple_lut.get) {
throw new TypeError('Expected a simple_value Map') }
return (ctx, key) => simple_lut.get(key)}
, build_tags_lut(tags) {
let lut = new Map();
let q = [tags];
while (0 !== q.length) {
let tip = q.pop();
if (Array.isArray(tip)) {
q.push(... tip);}
else if (tip[cbor_decode_sym]) {
tip[cbor_decode_sym](lut, cbor_accum);}
else if ('function' === typeof tip) {
tip(lut, cbor_accum);}
else {
for (let [k,v] of tip.entries()) {
lut.set(k,v);} } }
return lut}
, // Subclass responsibility: cbor size/value interpreters
// cbor_tiny(as_type) :: return function w0_as(ctx, type_b) ::
// cbor_w1(as_type) :: return function w1_as(ctx) ::
// cbor_w2(as_type) :: return function w2_as(ctx) ::
// cbor_w4(as_type) :: return function w4_as(ctx) ::
// cbor_w8(as_type) :: return function w8_as(ctx) ::
// Subclass responsibility: basic types
// as_bytes(ctx, len) ::
// as_utf8(ctx, len) ::
// as_list(ctx, len) ::
// as_map(ctx, len) ::
// Subclass responsibility: streaming types
// as_stream(ctx, accum) ::
// as_pair_stream(ctx, accum) ::
// Subclass responsibility: floating point primitives
// as_float16(ctx) :: return ctx.types.float16(...)
// as_float32(ctx) ::
// as_float64(ctx) ::
// Subclass responsibility: tag values
};// bind_tag_dispatch(tags_lut) ::
const _cbor_jmp_sync ={
__proto__: _cbor_jmp_base
, // cbor size/value interpreters
cbor_tiny(as_type) {
return function w0_as(ctx, type_b) {
return as_type(ctx, type_b & 0x1f) } }
, cbor_w1(as_type) {
return function w1_as(ctx) {
const idx = ctx.move(1);
return as_type(ctx, ctx.u8[idx]) } }
, cbor_w2(as_type) {
return function w2_as(ctx) {
const u8 = ctx.u8, idx = ctx.move(2);
return as_type(ctx, (u8[idx] << 8) | u8[idx+1]) } }
, cbor_w4(as_type) {
return function w4_as(ctx) {
const u8 = ctx.u8, idx = ctx.move(4);
return as_type(ctx, ctx.types.u32(u8, idx)) } }
, cbor_w8(as_type) {
return function w8_as(ctx) {
const u8 = ctx.u8, idx = ctx.move(8);
return as_type(ctx, ctx.types.u64(u8, idx)) } }
, // basic types
as_bytes(ctx, len) {
const u8 = ctx.u8, idx = ctx.move(len);
return ctx.types.bytes(
u8.subarray(idx, idx + len)) }
, as_utf8(ctx, len) {
const u8 = ctx.u8, idx = ctx.move(len);
return ctx.types.utf8(
u8.subarray(idx, idx + len)) }
, as_list(ctx, len) {
const {res, accum, done} = ctx.types.list(len);
for (let i=0; i<len; i++) {
accum(res, i, ctx.next_value()); }
return undefined !== done ? done(res) : res}
, as_map(ctx, len) {
const {res, accum, done} = ctx.types.map(len);
for (let i=0; i<len; i++) {
const key = ctx.next_value();
const value = ctx.next_value();
accum(res, key, value); }
return undefined !== done ? done(res) : res}
, // streaming
as_stream(ctx, {res, accum, done}) {
let i = 0;
while (true) {
const value = ctx.next_value();
if (cbor_break_sym === value) {
return undefined !== done ? done(res) : res}
accum(res, i++, value); } }
, as_pair_stream(ctx, {res, accum, done}) {
while (true) {
const key = ctx.next_value();
if (cbor_break_sym === key) {
return undefined !== done ? done(res) : res}
accum(res, key, ctx.next_value()); } }
, // floating point primitives
as_float16(ctx) {
const u8 = ctx.u8, idx = ctx.move(2);
return ctx.types.float16(
u8.subarray(idx, idx+2)) }
, as_float32(ctx) {
const u8 = ctx.u8, idx = ctx.move(4);
return ctx.types.float32(u8, idx)}
, as_float64(ctx) {
const u8 = ctx.u8, idx = ctx.move(8);
return ctx.types.float64(u8, idx)}
, // tag values
bind_tag_dispatch(tags_lut) {
if ('function' !== typeof tags_lut.get) {
throw new TypeError('Expected a tags Map') }
return function(ctx, tag) {
const tag_handler = tags_lut.get(tag);
if (tag_handler) {
let res = tag_handler(ctx, tag);
if ('object' === typeof res) {
return res.custom_tag(ctx, tag)}
const body = ctx.next_value();
return undefined === res ? body : res(body)}
return {
__proto__: cbor_tagged_proto,
tag, body: ctx.next_value()} } } };
class CBORDecoderBasic extends CBORDecoderBase {
// decode(u8) ::
static decode(u8) {
return new this().decode(u8)}
// *iter_decode(u8) ::
static iter_decode(u8) {
return new this().iter_decode(u8)}
_bind_cbor_jmp(options, jmp) {
return _cbor_jmp_sync.bind_jmp(options, jmp)}
_bind_u8ctx(types, jmp, unknown) {
return (this.U8DecodeCtx || U8SyncDecodeCtx)
.subclass(types, jmp, unknown)} }
CBORDecoderBasic.compile({
types: decode_types});
class CBORDecoder extends CBORDecoderBasic {}
CBORDecoder.compile({
types: decode_types,
tags: basic_tags(new Map()),});
const {decode, iter_decode} = new CBORDecoder();
export default decode;
export { CBORDecoder, CBORDecoderBasic, _cbor_jmp_base, _cbor_jmp_sync, decode as cbor_decode, iter_decode as cbor_iter_decode, decode, iter_decode };
{
"private": true,
"version": "0.0.0",
"devDependencies": {
"rollup": "2.52.7"
},
"// last working devDependencies": {
"rollup": "2.48.0"
},
"// max recursion devDependencies": {
"rollup": "2.52.7"
},
"// memory core dump devDependencies": {
"rollup": "2.53.3"
},
"scripts": {
"build": "rollup -c",
"test": "rollup -c"
}
}
export default {
input: `./unittest.mjs`,
output: {
file: './cjs/mocha_unittest.cjs',
format: 'cjs',
},
}
import {CBORDecoderBasic} from './cbor_codec_decode.mjs'
CBORDecoderBasic.options({})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment