zuzu-js 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -254,7 +254,7 @@ function stripShebang( source ) {
254
254
  }
255
255
 
256
256
  function printVersion( runtime, verbose ) {
257
- process.stdout.write( 'zuzu-js version 0.2.0\n' );
257
+ process.stdout.write( 'zuzu-js version 0.4.0\n' );
258
258
  if ( verbose ) {
259
259
  process.stdout.write( '\nlib search paths:\n' );
260
260
  for ( const p of runtime.getModuleSearchRoots() ) {
@@ -24,6 +24,10 @@ function makeSet( values ) {
24
24
  return runtimeHelpers().makeSet( values );
25
25
  }
26
26
 
27
+ function stringSortComparator( a, b ) {
28
+ return runtimeHelpers().stringSortComparator( a, b );
29
+ }
30
+
27
31
  class ZuzuBag {
28
32
  constructor( ...items ) {
29
33
  let values;
@@ -105,7 +109,7 @@ class ZuzuBag {
105
109
  sum() { return this.items.reduce( (a, b) => Number( a ) + Number( b ), 0 ); }
106
110
  product() { return this.items.reduce( (a, b) => Number( a ) * Number( b ), 1 ); }
107
111
  sort( fn ) { return this.to_Array().sort( fn ); }
108
- sortstr() { return this.to_Array().sort( (a, b) => String( a ).localeCompare( String( b ) ) ); }
112
+ sortstr() { return this.to_Array().sort( stringSortComparator ); }
109
113
  sortnum() { return this.to_Array().map( (item) => Number( item ) ).sort( (a, b) => a - b ); }
110
114
  clear() { releaseCollectionValues( this ); this.items = []; return this; }
111
115
  [Symbol.iterator]() { return this.items[Symbol.iterator](); }
@@ -338,7 +342,7 @@ function withArrayMethods() {
338
342
  define( 'shuffle', function _shuffle() { return this.slice(); } );
339
343
  define( 'sample', function _sample( n ) { return this.slice( 0, n ); } );
340
344
  define( 'for_each_value', function _for_each_value( fn ) { this.forEach( fn ); return this; } );
341
- define( 'sortstr', function _sortstr() { return this.slice().sort( (a, b) => String( a ).localeCompare( String( b ) ) ); } );
345
+ define( 'sortstr', function _sortstr() { return this.slice().sort( stringSortComparator ); } );
342
346
  define(
343
347
  'sortnum',
344
348
  function _sortnum() {
@@ -886,6 +886,40 @@ function operatorString( value ) {
886
886
  throw new Error( `TypeException: cannot coerce ${typeName( value )} to String` );
887
887
  }
888
888
 
889
+ const surrogatePattern = /[\uD800-\uDFFF]/;
890
+
891
+ // Compare two native strings by Unicode code point, matching the string
892
+ // ordering used by zuzu-rust and zuzu-perl. Plain UTF-16 comparison only
893
+ // disagrees with code-point order when astral characters (surrogate
894
+ // pairs) meet code units in the U+E000..U+FFFF range, so the slow path is
895
+ // only taken when a surrogate is present.
896
+ function codePointStringCompare( left, right ) {
897
+ if ( left === right ) {
898
+ return 0;
899
+ }
900
+ if ( surrogatePattern.test( left ) || surrogatePattern.test( right ) ) {
901
+ const leftPoints = Array.from( left );
902
+ const rightPoints = Array.from( right );
903
+ const shared = Math.min( leftPoints.length, rightPoints.length );
904
+ for ( let i = 0; i < shared; i++ ) {
905
+ const a = leftPoints[i].codePointAt( 0 );
906
+ const b = rightPoints[i].codePointAt( 0 );
907
+ if ( a !== b ) {
908
+ return a < b ? -1 : 1;
909
+ }
910
+ }
911
+ if ( leftPoints.length === rightPoints.length ) {
912
+ return 0;
913
+ }
914
+ return leftPoints.length < rightPoints.length ? -1 : 1;
915
+ }
916
+ return left < right ? -1 : 1;
917
+ }
918
+
919
+ function stringSortComparator( a, b ) {
920
+ return codePointStringCompare( String( a ), String( b ) );
921
+ }
922
+
889
923
  function operatorRegexp( value ) {
890
924
  value = resolveWeakValue( value );
891
925
  if ( value instanceof RegExp || Object.prototype.toString.call( value ) === '[object RegExp]' ) {
@@ -915,6 +949,8 @@ module.exports = {
915
949
  lengthOf,
916
950
  operatorString,
917
951
  operatorRegexp,
952
+ codePointStringCompare,
953
+ stringSortComparator,
918
954
  ZuzuBinary,
919
955
  BinaryString,
920
956
  ZuzuWeakCell,
package/lib/runtime.js CHANGED
@@ -46,6 +46,8 @@ const {
46
46
  addSetValue,
47
47
  assignStrongValue,
48
48
  assignWeakValue,
49
+ codePointStringCompare,
50
+ stringSortComparator,
49
51
  } = require( './runtime-helpers' );
50
52
  const taskRuntime = require( '../modules/std/task' );
51
53
 
@@ -53,6 +55,28 @@ const textEncoder = new TextEncoder();
53
55
  const utf8Decoder = new TextDecoder( 'utf-8', { fatal: true } );
54
56
  const ZUZU_SKIP_BUILD = Symbol.for( 'zuzu.skip_build' );
55
57
 
58
+ const runtimeVersion = (() => {
59
+ if ( typeof globalThis === 'object' && globalThis !== null ) {
60
+ const globalVersion = globalThis.__ZUZU_RUNTIME_VERSION;
61
+ if ( typeof globalVersion === 'string' && globalVersion.trim() ) {
62
+ return String( globalVersion );
63
+ }
64
+ }
65
+ if ( typeof process === 'object' && process && process.versions ) {
66
+ try {
67
+ const packageMetadata = require( '../package.json' );
68
+ const packageVersion = packageMetadata && packageMetadata.version;
69
+ if ( packageVersion ) {
70
+ return String( packageVersion );
71
+ }
72
+ }
73
+ catch ( err ) {
74
+ return 'dev';
75
+ }
76
+ }
77
+ return 'dev';
78
+ })();
79
+
56
80
  function defaultModuleSearchRoots( host, repoRoot, includePaths ) {
57
81
  if ( host.name === 'browser' ) {
58
82
  return [
@@ -226,11 +250,21 @@ function stringCompare( left, right, options = {} ) {
226
250
  const r = options.insensitive
227
251
  ? zuzuOperatorString( right ).toLowerCase()
228
252
  : zuzuOperatorString( right );
229
- return l.localeCompare( r );
253
+ // Code-point order, matching zuzu-rust and zuzu-perl; localeCompare
254
+ // would give locale-dependent, case-folded ordering.
255
+ return codePointStringCompare( l, r );
230
256
  }
231
257
 
232
258
  function parseNumericString( value ) {
233
259
  const text = String( value ).trim();
260
+ // Coercion is more permissive than the lexer: radix prefixes match
261
+ // either case.
262
+ const radix = text.match( /^([+-]?)0([xX][0-9a-fA-F]+|[bB][01]+|[oO][0-7]+)$/ );
263
+ if ( radix ) {
264
+ const base = 'xX'.includes( radix[2][0] ) ? 16 : 'bB'.includes( radix[2][0] ) ? 2 : 8;
265
+ const magnitude = parseInt( radix[2].slice( 1 ), base );
266
+ return radix[1] === '-' ? -magnitude : magnitude;
267
+ }
234
268
  const match = text.match( /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/ );
235
269
  if ( !match ) {
236
270
  throw new Error( `TypeException: cannot coerce String to Number: ${JSON.stringify( String( value ) )}` );
@@ -565,6 +599,52 @@ function bitwiseNot( value ) {
565
599
  return ( ~( zuzuToNumber( value ) >>> 0 ) ) >>> 0;
566
600
  }
567
601
 
602
+ // BinaryStrings shift as one whole bit string: bits carry across byte
603
+ // boundaries, length is preserved, vacated bits are 0. Numbers shift via
604
+ // float multiply/divide so counts beyond 32 bits behave portably.
605
+ function shiftBitstream( bytes, count, left ) {
606
+ const len = bytes.length;
607
+ const out = new Uint8Array( len );
608
+ if ( len === 0 || count >= len * 8 ) {
609
+ return out;
610
+ }
611
+ const byteShift = Math.floor( count / 8 );
612
+ const bitShift = count % 8;
613
+ for ( let i = 0; i < len; i++ ) {
614
+ let value;
615
+ if ( left ) {
616
+ const src = i + byteShift;
617
+ const hi = src < len ? bytes[src] : 0;
618
+ const lo = src + 1 < len ? bytes[src + 1] : 0;
619
+ value = ( ( ( hi << 8 ) | lo ) << bitShift ) >> 8;
620
+ }
621
+ else {
622
+ if ( i < byteShift ) {
623
+ continue;
624
+ }
625
+ const lo = bytes[i - byteShift];
626
+ const hi = i > byteShift ? bytes[i - byteShift - 1] : 0;
627
+ value = ( ( hi << 8 ) | lo ) >> bitShift;
628
+ }
629
+ out[i] = value & 0xff;
630
+ }
631
+ return out;
632
+ }
633
+
634
+ function shiftValue( left, right, leftward ) {
635
+ left = resolveWeakValue( left );
636
+ const count = Math.trunc( zuzuToNumber( right ) );
637
+ if ( !( count >= 0 ) || !Number.isFinite( count ) ) {
638
+ throw new Error( 'Exception: shift count must be a non-negative integer' );
639
+ }
640
+ if ( left instanceof ZuzuBinary ) {
641
+ return new BinaryString( shiftBitstream( left.bytes, count, leftward ) );
642
+ }
643
+ const value = Math.trunc( zuzuToNumber( left ) );
644
+ const factor = Math.pow( 2, count );
645
+ return leftward ? value * factor : Math.floor( value / factor );
646
+ }
647
+
568
648
  function zuzuComparableValue( value ) {
569
649
  value = resolveWeakValue( value );
570
650
  if ( typeof value === 'function' && value.length === 0 ) {
@@ -1283,7 +1363,11 @@ function defineZuzuClass( name, base, spec = {} ) {
1283
1363
  zuzuStoreField( this, field, value );
1284
1364
  }
1285
1365
  let named = null;
1286
- if ( ctorArgs.length === 1 && isPairListLike( ctorArgs[0] ) ) {
1366
+ const lastArg = ctorArgs[ctorArgs.length - 1];
1367
+ if (
1368
+ ctorArgs.length === 1
1369
+ && isPairListLike( ctorArgs[0] )
1370
+ ) {
1287
1371
  named = ctorArgs[0];
1288
1372
  }
1289
1373
  else if (
@@ -1294,6 +1378,20 @@ function defineZuzuClass( name, base, spec = {} ) {
1294
1378
  ) {
1295
1379
  named = ctorArgs[0];
1296
1380
  }
1381
+ else if (
1382
+ ctorArgs.length > 1
1383
+ && isPairListLike( lastArg )
1384
+ ) {
1385
+ named = lastArg;
1386
+ }
1387
+ else if (
1388
+ ctorArgs.length > 1
1389
+ && lastArg
1390
+ && typeof lastArg === 'object'
1391
+ && !Array.isArray( lastArg )
1392
+ ) {
1393
+ named = lastArg;
1394
+ }
1297
1395
  const releaseTemporaryNamed = !skipBuild
1298
1396
  && isPairListLike( named )
1299
1397
  && named.__zuzu_temporary_call_args === true;
@@ -1459,9 +1557,6 @@ function zuzuCallMember( object, property, ...args ) {
1459
1557
  if ( dictMethod ) {
1460
1558
  return dictMethod( ...args );
1461
1559
  }
1462
- if ( args.length === 0 ) {
1463
- return value;
1464
- }
1465
1560
  throw new TypeError( `${String( property )} is not a function` );
1466
1561
  }
1467
1562
 
@@ -2128,7 +2223,13 @@ class ZuzuScript {
2128
2223
  }
2129
2224
  const moduleSearchRoots = this.getModuleSearchRoots();
2130
2225
  const emit = ( value ) => {
2131
- const line = String( value );
2226
+ let line = String( value );
2227
+ if ( line === 'Infinity' ) {
2228
+ line = 'Inf';
2229
+ }
2230
+ else if ( line === '-Infinity' ) {
2231
+ line = '-Inf';
2232
+ }
2132
2233
  if ( this.outputLines ) {
2133
2234
  this.outputLines.push( line );
2134
2235
  }
@@ -2387,7 +2488,7 @@ class ZuzuScript {
2387
2488
  defineRuntimeMethod( Set.prototype, 'is_disjoint', function _setIsDisjoint( other ) { return this.intersection( other ).size === 0 ? 1 : 0; } );
2388
2489
  defineRuntimeMethod( Set.prototype, 'equals', function _setEquals( other ) { return collectionEquivalentOf( this, other ); } );
2389
2490
  defineRuntimeMethod( Set.prototype, 'sort', function _setSort( fn ) { return [ ...this ].sort( fn ); } );
2390
- defineRuntimeMethod( Set.prototype, 'sortstr', function _setSortstr() { return [ ...this ].sort( (a, b) => String( a ).localeCompare( String( b ) ) ); } );
2491
+ defineRuntimeMethod( Set.prototype, 'sortstr', function _setSortstr() { return [ ...this ].sort( stringSortComparator ); } );
2391
2492
  defineRuntimeMethod( Set.prototype, 'sortnum', function _setSortnum() { return [ ...this ].map( (item) => Number( item ) ).sort( (a, b) => a - b ); } );
2392
2493
  defineRuntimeMethod( Set.prototype, 'map', function _setMap( fn ) { return new Set( [ ...this ].map( fn ) ); } );
2393
2494
  defineRuntimeMethod( Set.prototype, 'grep', function _setGrep( fn ) { return new Set( [ ...this ].filter( fn ) ); } );
@@ -2421,7 +2522,7 @@ class ZuzuScript {
2421
2522
  const systemGlobalSeed = {
2422
2523
  language_version: 0,
2423
2524
  runtime: 'zuzu-js',
2424
- runtime_version: 'dev',
2525
+ runtime_version: runtimeVersion,
2425
2526
  platform: this.host.name,
2426
2527
  inc: Object.freeze( moduleSearchRoots.slice() ),
2427
2528
  deny_fs: capabilityFlags.fs ? false : true,
@@ -2767,6 +2868,14 @@ class ZuzuScript {
2767
2868
  if ( input == null ) {
2768
2869
  return [];
2769
2870
  }
2871
+ if ( input instanceof ZuzuBinary ) {
2872
+ // Iterate byte-by-byte, each as a 1-byte BinaryString.
2873
+ return Array.from( input.bytes, (byte) => new BinaryString( [ byte ] ) );
2874
+ }
2875
+ if ( typeof input === 'string' || input instanceof String ) {
2876
+ // Iterate character-by-character (code points).
2877
+ return Array.from( String( input ) );
2878
+ }
2770
2879
  if ( typeof input[Symbol.iterator] === 'function' ) {
2771
2880
  return input;
2772
2881
  }
@@ -2880,6 +2989,15 @@ class ZuzuScript {
2880
2989
  return value.isAscii();
2881
2990
  },
2882
2991
  __zuzu_bit_and( left, right ) { return bitwiseAnd( left, right ); },
2992
+ __zuzu_shift_left( left, right ) { return shiftValue( left, right, true ); },
2993
+ // The left operand is the divisor: a ∣ b tests b mod a.
2994
+ __zuzu_divides( left, right ) {
2995
+ return ( zuzuToNumber( right ) % zuzuToNumber( left ) ) === 0;
2996
+ },
2997
+ __zuzu_ndivides( left, right ) {
2998
+ return zuzuToNumber( right ) % zuzuToNumber( left );
2999
+ },
3000
+ __zuzu_shift_right( left, right ) { return shiftValue( left, right, false ); },
2883
3001
  __zuzu_bit_or( left, right ) { return bitwiseOr( left, right ); },
2884
3002
  __zuzu_bit_xor( left, right ) { return bitwiseXor( left, right ); },
2885
3003
  __zuzu_bit_not( value ) { return bitwiseNot( value ); },
@@ -3169,7 +3287,7 @@ class ZuzuScript {
3169
3287
  define( Array.prototype, 'shuffle', function _shuffle() { return this.slice(); } );
3170
3288
  define( Array.prototype, 'sample', function _sample( n ) { return this.slice( 0, n ); } );
3171
3289
  define( Array.prototype, 'for_each_value', function _for_each_value( fn ) { this.forEach( fn ); return this; } );
3172
- define( Array.prototype, 'sortstr', function _sortstr() { return this.slice().sort( (a, b) => String( a ).localeCompare( String( b ) ) ); } );
3290
+ define( Array.prototype, 'sortstr', function _sortstr() { return this.slice().sort( stringSortComparator ); } );
3173
3291
  define( Array.prototype, 'sortnum', function _sortnum() { return this.map( (item) => Number( item ) ).sort( (a, b) => a - b ); } );
3174
3292
  define( Array.prototype, 'to_Array', function _to_array() { return __zuzu_array( this ); } );
3175
3293
  define( Array.prototype, 'to_Set', function _to_set() { return __zuzu_set( this ); } );
@@ -3199,7 +3317,7 @@ class ZuzuScript {
3199
3317
  define( Set.prototype, 'is_disjoint', function _is_disjoint( other ) { return this.intersection( other ).size === 0 ? 1 : 0; } );
3200
3318
  define( Set.prototype, 'equals', function _equals( other ) { return __zuzu_equivalentof( this, other ); } );
3201
3319
  define( Set.prototype, 'sort', function _sort( fn ) { return [ ...this ].sort( fn ); } );
3202
- define( Set.prototype, 'sortstr', function _sortstr() { return [ ...this ].sort( (a, b) => String( a ).localeCompare( String( b ) ) ); } );
3320
+ define( Set.prototype, 'sortstr', function _sortstr() { return [ ...this ].sort( stringSortComparator ); } );
3203
3321
  define( Set.prototype, 'sortnum', function _sortnum() { return [ ...this ].map( (item) => Number( item ) ).sort( (a, b) => a - b ); } );
3204
3322
  define( Set.prototype, 'map', function _map( fn ) { return new Set( [ ...this ].map( fn ) ); } );
3205
3323
  define( Set.prototype, 'grep', function _grep( fn ) { return new Set( [ ...this ].filter( fn ) ); } );
@@ -609,6 +609,8 @@ function extractSpecialPreludeNames( signature ) {
609
609
  return [ '__zuzu_call_args', signature.headName, signature.restName, signature.namedName, '__i', '__a' ];
610
610
  case 'scalar_pairlist':
611
611
  return [ '__zuzu_call_args', signature.headName, signature.namedName, '__i', '__a' ];
612
+ case 'scalar_pairlist_array':
613
+ return [ '__zuzu_call_args', signature.headName, signature.namedName, signature.restName, '__i', '__a' ];
612
614
  case 'variadic':
613
615
  return [ '__zuzu_call_args', signature.headName, signature.restName, '__i', '__a' ];
614
616
  default:
@@ -694,6 +696,22 @@ function analyzeSpecialSignature( params ) {
694
696
  namedName: params[1].name,
695
697
  };
696
698
  }
699
+ if (
700
+ params.length === 3
701
+ && params[0].type === 'Parameter'
702
+ && params[1].type === 'SpecialParameter'
703
+ && params[1].special === 'rest_only'
704
+ && params[1].containerType === 'PairList'
705
+ && params[2].type === 'Parameter'
706
+ && ( params[2].typeName === 'Array' || params[2].typeName == null )
707
+ ) {
708
+ return {
709
+ kind: 'scalar_pairlist_array',
710
+ headName: params[0].name,
711
+ namedName: params[1].name,
712
+ restName: params[2].name,
713
+ };
714
+ }
697
715
  if (
698
716
  params.length === 1
699
717
  && params[0].type === 'SpecialParameter'
@@ -835,6 +853,19 @@ function emitSpecialFunctionPreamble( signature ) {
835
853
  '}',
836
854
  `const ${signature.namedName} = __zuzu_named_args;`,
837
855
  ].join( '\n' );
856
+ case 'scalar_pairlist_array':
857
+ return [
858
+ 'const __zuzu_call_args = Array.prototype.slice.call( arguments );',
859
+ `const ${signature.headName} = __zuzu_call_args[0];`,
860
+ 'let __zuzu_named_args = __zuzu_pairlist_literal( [] );',
861
+ 'const __zuzu_rest_args = [];',
862
+ 'for ( let __i = 1; __i < __zuzu_call_args.length; __i++ ) {',
863
+ 'const __a = __zuzu_call_args[__i];',
864
+ 'if ( __zuzu_is_pairlist( __a ) ) { __zuzu_named_args = __a; } else { __zuzu_rest_args.push( __a ); }',
865
+ '}',
866
+ `const ${signature.restName} = __zuzu_rest_args;`,
867
+ `const ${signature.namedName} = __zuzu_named_args;`,
868
+ ].join( '\n' );
838
869
  case 'variadic':
839
870
  return [
840
871
  'const __zuzu_call_args = Array.prototype.slice.call( arguments );',
@@ -2121,6 +2152,17 @@ function emitBinaryExpression( node ) {
2121
2152
  return `__zuzu_bit_or( ${left}, ${right} )`;
2122
2153
  case '^':
2123
2154
  return `__zuzu_bit_xor( ${left}, ${right} )`;
2155
+ case '<<':
2156
+ case '«':
2157
+ return `__zuzu_shift_left( ${left}, ${right} )`;
2158
+ case '∣':
2159
+ case 'divides':
2160
+ return `__zuzu_divides( ${left}, ${right} )`;
2161
+ case '∤':
2162
+ return `__zuzu_ndivides( ${left}, ${right} )`;
2163
+ case '>>':
2164
+ case '»':
2165
+ return `__zuzu_shift_right( ${left}, ${right} )`;
2124
2166
  default:
2125
2167
  return `( ${left} ${node.operator} ${right} )`;
2126
2168
  }
@@ -2163,6 +2205,9 @@ function emitAssignmentExpression( node ) {
2163
2205
  : `${left} = __zuzu_assign_strong( ${left}, ${right} )`;
2164
2206
  }
2165
2207
  if ( node.left && node.left.type === 'MemberExpression' ) {
2208
+ if ( !node.left.computed ) {
2209
+ throw new UnsupportedSyntaxError( 'Invalid assignment target' );
2210
+ }
2166
2211
  const object = emitExpression( node.left.object );
2167
2212
  const objectTarget = emitAssignmentTarget( node.left.object );
2168
2213
  const key = node.left.computed
@@ -103,6 +103,7 @@ const KEYWORDS = new Set( [
103
103
  'throw',
104
104
  'sub',
105
105
  'union',
106
+ 'divides',
106
107
  'intersection',
107
108
  'difference',
108
109
  'subsetof',
@@ -772,6 +773,29 @@ function tokenize( source ) {
772
773
  }
773
774
 
774
775
  if ( isDigit( ch ) ) {
776
+ // Radix-prefixed integers: 0x… hex, 0b… binary, 0o… octal.
777
+ // Lowercase prefixes only; the token value is normalised to
778
+ // decimal so later stages treat it as a plain number.
779
+ const radixDigitOk = ch === '0'
780
+ ? ( peek( 1 ) === 'x'
781
+ ? (c) => /[0-9a-fA-F]/.test( c || '' )
782
+ : peek( 1 ) === 'b'
783
+ ? (c) => c === '0' || c === '1'
784
+ : peek( 1 ) === 'o'
785
+ ? (c) => /[0-7]/.test( c || '' )
786
+ : null )
787
+ : null;
788
+ if ( radixDigitOk && radixDigitOk( peek( 2 ) ) ) {
789
+ const radix = peek( 1 ) === 'x' ? 16 : peek( 1 ) === 'b' ? 2 : 8;
790
+ advance();
791
+ advance();
792
+ let digits = '';
793
+ while ( radixDigitOk( peek() ) ) {
794
+ digits += advance();
795
+ }
796
+ addToken( 'number', String( parseInt( digits, radix ) ), start, cursor() );
797
+ continue;
798
+ }
775
799
  let value = '';
776
800
  while ( isDigit( peek() ) ) {
777
801
  value += advance();
@@ -782,6 +806,21 @@ function tokenize( source ) {
782
806
  value += advance();
783
807
  }
784
808
  }
809
+ // Exponent: uppercase E only (lowercase e is not part of
810
+ // the language).
811
+ if (
812
+ peek() === 'E'
813
+ && ( isDigit( peek( 1 ) )
814
+ || ( ( peek( 1 ) === '+' || peek( 1 ) === '-' ) && isDigit( peek( 2 ) ) ) )
815
+ ) {
816
+ value += advance();
817
+ if ( peek() === '+' || peek() === '-' ) {
818
+ value += advance();
819
+ }
820
+ while ( isDigit( peek() ) ) {
821
+ value += advance();
822
+ }
823
+ }
785
824
  addToken( 'number', value, start, cursor() );
786
825
  continue;
787
826
  }
@@ -812,7 +851,7 @@ function tokenize( source ) {
812
851
  continue;
813
852
  }
814
853
 
815
- if ( [ '≡', '≢', '≠', '≤', '≥', '→', '√', '⌊', '⌋', '⌈', '⌉', '∈', '∉', '⋃', '⋂', '∖', '⊂', '⊃', '«', '»', '≶', '≷', '▷', '◁' ].includes( ch ) ) {
854
+ if ( [ '≡', '≢', '≠', '≤', '≥', '→', '√', '⌊', '⌋', '⌈', '⌉', '∈', '∉', '⋃', '⋂', '∖', '⊂', '⊃', '«', '»', '≶', '≷', '▷', '◁', '∣', '∤' ].includes( ch ) ) {
816
855
  advance();
817
856
  addToken( 'operator', ch, start, cursor() );
818
857
  continue;
@@ -8,6 +8,7 @@ const {
8
8
  } = require( './errors' );
9
9
 
10
10
  function parse( tokens, options = {} ) {
11
+ const pendingSetClosers = [];
11
12
  let index = 0;
12
13
  let asyncContextDepth = 1;
13
14
 
@@ -1541,20 +1542,31 @@ function parse( tokens, options = {} ) {
1541
1542
 
1542
1543
  function parseBitwise() {
1543
1544
  return parseLeftAssociative(
1544
- parseComparison,
1545
+ parseShift,
1545
1546
  (token) => token.type === 'operator' && [ '&', '|', '^' ].includes( token.value )
1546
1547
  );
1547
1548
  }
1548
1549
 
1550
+ function parseShift() {
1551
+ return parseLeftAssociative(
1552
+ parseComparison,
1553
+ (token) => token.type === 'operator'
1554
+ && [ '<<', '>>', '«', '»' ].includes( token.value )
1555
+ // Inside << ... >> or « ... » the pending closer ends the
1556
+ // set literal instead of acting as a shift operator.
1557
+ && token.value !== pendingSetClosers[pendingSetClosers.length - 1]
1558
+ );
1559
+ }
1560
+
1549
1561
  function parseComparison() {
1550
1562
  return parseLeftAssociative(
1551
1563
  parseRange,
1552
1564
  (token) => (
1553
1565
  token.type === 'operator'
1554
- && [ '<', '<=', '>', '>=', '≤', '≥', '~', '<=>', '@', '@@', '@?', '\\', '∈', '∉', '⋃', '⋂', '∖', '⊂', '⊃', '⊂⊃', '≶', '≷' ].includes( token.value )
1566
+ && [ '<', '<=', '>', '>=', '≤', '≥', '~', '<=>', '@', '@@', '@?', '\\', '∈', '∉', '⋃', '⋂', '∖', '⊂', '⊃', '⊂⊃', '≶', '≷', '∣', '∤' ].includes( token.value )
1555
1567
  ) || (
1556
1568
  token.type === 'keyword'
1557
- && [ 'gt', 'ge', 'lt', 'le', 'cmp', 'eqi', 'nei', 'gti', 'gei', 'lti', 'lei', 'cmpi', 'in', 'not_in', 'union', 'intersection', 'difference', 'subsetof', 'supersetof', 'equivalentof', 'does', 'can' ].includes( token.value )
1569
+ && [ 'gt', 'ge', 'lt', 'le', 'cmp', 'eqi', 'nei', 'gti', 'gei', 'lti', 'lei', 'cmpi', 'in', 'not_in', 'union', 'intersection', 'difference', 'subsetof', 'supersetof', 'equivalentof', 'does', 'can', 'divides' ].includes( token.value )
1558
1570
  ) || (
1559
1571
  token.type === 'identifier' && token.value === 'instanceof'
1560
1572
  )
@@ -2252,18 +2264,24 @@ function parse( tokens, options = {} ) {
2252
2264
  if ( match( 'operator', closer ) ) {
2253
2265
  return elements;
2254
2266
  }
2255
- while ( true ) {
2256
- if ( match( 'punctuation', ',' ) ) {
2257
- continue;
2258
- }
2259
- if ( match( 'operator', closer ) ) {
2260
- break;
2261
- }
2262
- elements.push( parseExpression() );
2263
- if ( match( 'operator', closer ) ) {
2264
- break;
2267
+ pendingSetClosers.push( closer );
2268
+ try {
2269
+ while ( true ) {
2270
+ if ( match( 'punctuation', ',' ) ) {
2271
+ continue;
2272
+ }
2273
+ if ( match( 'operator', closer ) ) {
2274
+ break;
2275
+ }
2276
+ elements.push( parseExpression() );
2277
+ if ( match( 'operator', closer ) ) {
2278
+ break;
2279
+ }
2280
+ match( 'punctuation', ',' );
2265
2281
  }
2266
- match( 'punctuation', ',' );
2282
+ }
2283
+ finally {
2284
+ pendingSetClosers.pop();
2267
2285
  }
2268
2286
  return elements;
2269
2287
  }
@@ -235,6 +235,7 @@ function validateExpression( node, scope ) {
235
235
  }
236
236
  return;
237
237
  case 'UpdateExpression':
238
+ requireValidAssignmentTarget( node.argument );
238
239
  if ( node.argument && node.argument.type === 'Identifier' ) {
239
240
  requireMutable( node.argument, scope );
240
241
  }
@@ -242,6 +243,23 @@ function validateExpression( node, scope ) {
242
243
  validateExpression( node.argument, scope );
243
244
  }
244
245
  return;
246
+ case 'UnaryExpression':
247
+ if ( node.operator === '++' || node.operator === '--' ) {
248
+ requireValidAssignmentTarget( node.argument );
249
+ if ( node.argument && node.argument.type === 'Identifier' ) {
250
+ requireMutable( node.argument, scope );
251
+ }
252
+ else {
253
+ validateExpression( node.argument, scope );
254
+ }
255
+ return;
256
+ }
257
+ validateChildNodes( node, scope );
258
+ return;
259
+ case 'RefExpression':
260
+ requireValidAssignmentTarget( node.argument );
261
+ validateExpression( node.argument, scope );
262
+ return;
245
263
  case 'FunctionExpression':
246
264
  validateFunctionBody( node, scope );
247
265
  return;
@@ -261,11 +279,13 @@ function requireValidAssignmentTarget( node ) {
261
279
  }
262
280
  if (
263
281
  target.type === 'Identifier'
264
- || target.type === 'MemberExpression'
265
282
  || target.type === 'SliceExpression'
266
283
  ) {
267
284
  return;
268
285
  }
286
+ if ( target.type === 'MemberExpression' && target.computed ) {
287
+ return;
288
+ }
269
289
  if (
270
290
  target.type === 'BinaryExpression'
271
291
  && [ '@', '@@', '@?' ].includes( target.operator )