json-as 0.7.1 → 0.7.3

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.
@@ -15,7 +15,6 @@ function canSer<T>(data: T, toBe: string): void {
15
15
  expect(serialized).toBe(toBe);
16
16
  }
17
17
 
18
- // @ts-ignore
19
18
  @json
20
19
  class Map4 {
21
20
  a: string;
@@ -24,7 +23,6 @@ class Map4 {
24
23
  d: string;
25
24
  }
26
25
 
27
- // @ts-ignore
28
26
  @json
29
27
  class Vec3 {
30
28
  x: f64;
@@ -32,7 +30,6 @@ class Vec3 {
32
30
  z: f64;
33
31
  }
34
32
 
35
- // @ts-ignore
36
33
  @json
37
34
  class Player {
38
35
  firstName: string;
@@ -76,9 +73,18 @@ describe("Ser/de Numbers", () => {
76
73
  canSerde<f64>(10e2, "1000.0");
77
74
 
78
75
  canSerde<f64>(123456e-5, "1.23456");
79
-
80
76
  canSerde<f64>(0.0, "0.0");
81
- canSerde<f64>(7.23, "7.23");
77
+ canSerde<f64>(-7.23, "-7.23");
78
+
79
+ canSerde<f64>(1e-6, "0.000001");
80
+ canSerde<f64>(1e-7, "1e-7");
81
+ canDeser<f64>("1E-7", 1e-7);
82
+
83
+ canSerde<f64>(1e20, "100000000000000000000.0");
84
+ canSerde<f64>(1e21, "1e+21");
85
+ canDeser<f64>("1E+21", 1e21);
86
+ canDeser<f64>("1e21", 1e21);
87
+ canDeser<f64>("1E21", 1e21);
82
88
  });
83
89
 
84
90
  it("should ser/de booleans", () => {
@@ -100,6 +106,11 @@ describe("Ser/de Array", () => {
100
106
 
101
107
  it("should ser/de float arrays", () => {
102
108
  canSerde<f64[]>([7.23, 10e2, 10e2, 123456e-5, 123456e-5, 0.0, 7.23]);
109
+
110
+ canSerde<f64[]>([1e21,1e22,1e-7,1e-8,1e-9], "[1e+21,1e+22,1e-7,1e-8,1e-9]");
111
+ canDeser<f64[]>("[1E+21,1E+22,1E-7,1E-8,1E-9]", [1e21,1e22,1e-7,1e-8,1e-9]);
112
+ canDeser<f64[]>("[1e21,1e22,1e-7,1e-8,1e-9]", [1e21,1e22,1e-7,1e-8,1e-9]);
113
+ canDeser<f64[]>("[1E21,1E22,1E-7,1E-8,1E-9]", [1e21,1e22,1e-7,1e-8,1e-9]);
103
114
  });
104
115
 
105
116
  it("should ser/de boolean arrays", () => {
@@ -170,6 +181,38 @@ describe("Ser/de Objects", () => {
170
181
  isVerified: true,
171
182
  }, '{"firstName":"Emmet","lastName":"West","lastActive":[8,27,2022],"age":23,"pos":{"x":3.4,"y":1.2,"z":8.3},"isVerified":true}');
172
183
  });
184
+
185
+ it("should ser/de object with floats", () => {
186
+ canSerde<ObjectWithFloat>({ f: 7.23 }, '{"f":7.23}');
187
+ canSerde<ObjectWithFloat>({ f: 0.000001 }, '{"f":0.000001}');
188
+
189
+ canSerde<ObjectWithFloat>({ f: 1e-7 }, '{"f":1e-7}');
190
+ canDeser<ObjectWithFloat>('{"f":1E-7}', { f: 1e-7 });
191
+
192
+ canSerde<ObjectWithFloat>({ f: 1e20 }, '{"f":100000000000000000000.0}');
193
+ canSerde<ObjectWithFloat>({ f: 1e21 }, '{"f":1e+21}');
194
+ canDeser<ObjectWithFloat>('{"f":1E+21}', { f: 1e21 });
195
+ canDeser<ObjectWithFloat>('{"f":1e21}', { f: 1e21 });
196
+ });
197
+
198
+ it("should ser/de object with float arrays", () => {
199
+ canSerde<ObjectWithFloatArray>(
200
+ { fa: [1e21,1e22,1e-7,1e-8,1e-9] },
201
+ '{"fa":[1e+21,1e+22,1e-7,1e-8,1e-9]}');
202
+
203
+ canDeser<ObjectWithFloatArray>(
204
+ '{"fa":[1E+21,1E+22,1E-7,1E-8,1E-9]}',
205
+ { fa: [1e21,1e22,1e-7,1e-8,1e-9] });
206
+
207
+ canDeser<ObjectWithFloatArray>(
208
+ '{"fa":[1e21,1e22,1e-7,1e-8,1e-9]}',
209
+ { fa: [1e21,1e22,1e-7,1e-8,1e-9] });
210
+
211
+ canDeser<ObjectWithFloatArray>(
212
+ '{"fa":[1E21,1E22,1E-7,1E-8,1E-9]}',
213
+ { fa: [1e21,1e22,1e-7,1e-8,1e-9] });
214
+
215
+ });
173
216
  });
174
217
 
175
218
  describe("Ser externals", () => {
@@ -179,7 +222,6 @@ describe("Ser externals", () => {
179
222
  })
180
223
  });
181
224
 
182
- // @ts-ignore
183
225
  @json
184
226
  class HttpResp {
185
227
  statusCode: number;
@@ -347,3 +389,253 @@ describe("Ser/de Maps", () => {
347
389
  });
348
390
 
349
391
  });
392
+
393
+ describe("Ser/de escape sequences in strings", () => {
394
+ it("should encode short escape sequences", () => {
395
+ canSer("\\", '"\\\\"');
396
+ canSer('"', '"\\""');
397
+ canSer("\n", '"\\n"');
398
+ canSer("\r", '"\\r"');
399
+ canSer("\t", '"\\t"');
400
+ canSer("\b", '"\\b"');
401
+ canSer("\f", '"\\f"');
402
+ });
403
+
404
+ it("should decode short escape sequences", () => {
405
+ canDeser('"\\\\"', "\\");
406
+ canDeser('"\\""', '"');
407
+ canDeser('"\\n"', "\n");
408
+ canDeser('"\\r"', "\r");
409
+ canDeser('"\\t"', "\t");
410
+ canDeser('"\\b"', "\b");
411
+ canDeser('"\\f"', "\f");
412
+ });
413
+
414
+ it("should decode escaped forward slash but not encode", () => {
415
+ canSer("/", '"/"');
416
+ canDeser('"/"', "/");
417
+ canDeser('"\\/"', "/"); // allowed
418
+ });
419
+
420
+ // 0x00 - 0x1f, excluding characters that have short escape sequences
421
+ it("should encode long escape sequences", () => {
422
+ const singles = ["\n", "\r", "\t", "\b", "\f"];
423
+ for (let i = 0; i < 0x1F; i++) {
424
+ const c = String.fromCharCode(i);
425
+ if (singles.includes(c)) continue;
426
+ const actual = JSON.stringify(c);
427
+ const expected = `"\\u${i.toString(16).padStart(4, "0")}"`;
428
+ expect(actual).toBe(expected, `Failed to encode '\\x${i.toString(16).padStart(2, "0")}'`);
429
+ }
430
+ });
431
+
432
+ // \u0000 - \u001f
433
+ it("should decode long escape sequences (lower cased)", () => {
434
+ for (let i = 0; i <= 0x1f; i++) {
435
+ const s = `"\\u${i.toString(16).padStart(4, "0").toLowerCase()}"`;
436
+ const actual = JSON.parse<string>(s);
437
+ const expected = String.fromCharCode(i);
438
+ expect(actual).toBe(expected, `Failed to decode ${s}`);
439
+ }
440
+ });
441
+
442
+ // \u0000 - \u001F
443
+ it("should decode long escape sequences (upper cased)", () => {
444
+ for (let i = 0; i <= 0x1f; i++) {
445
+ const s = `"\\u${i.toString(16).padStart(4, "0").toUpperCase()}"`;
446
+ const actual = JSON.parse<string>(s);
447
+ const expected = String.fromCharCode(i);
448
+ expect(actual).toBe(expected, `Failed to decode ${s}`);
449
+ }
450
+ });
451
+
452
+ // See https://datatracker.ietf.org/doc/html/rfc8259#section-7
453
+ it("should decode UTF-16 surrogate pairs", () => {
454
+ const s = '"\\uD834\\uDD1E"';
455
+ const actual = JSON.parse<string>(s);
456
+ const expected = "𝄞";
457
+ expect(actual).toBe(expected);
458
+ });
459
+
460
+ // Just because we can decode UTF-16 surrogate pairs, doesn't mean we should encode them.
461
+ it("should not encode UTF-16 surrogate pairs", () => {
462
+ const s = "𝄞";
463
+ const actual = JSON.stringify(s);
464
+ const expected = '"𝄞"';
465
+ expect(actual).toBe(expected);
466
+ });
467
+
468
+ it("should encode multiple escape sequences", () => {
469
+ canSer('"""', '"\\"\\"\\""');
470
+ canSer('\\\\\\', '"\\\\\\\\\\\\"');
471
+ });
472
+
473
+ it("cannot parse invalid escape sequences", () => {
474
+ expect(() => {
475
+ JSON.parse<string>('"\\z"');
476
+ }).toThrow();
477
+ });
478
+
479
+ });
480
+
481
+ describe("Ser/de special strings in object values", () => {
482
+ it("should serialize quotes in string in object", () => {
483
+ const o: ObjWithString = { s: '"""' };
484
+ const s = '{"s":"\\"\\"\\""}';
485
+ canSer(o, s);
486
+ });
487
+ it("should deserialize quotes in string in object", () => {
488
+ const o: ObjWithString = { s: '"""' };
489
+ const s = '{"s":"\\"\\"\\""}';
490
+ canDeser(s, o);
491
+ });
492
+ it("should serialize backslashes in string in object", () => {
493
+ const o: ObjWithString = { s: "\\\\\\" };
494
+ const s = '{"s":"\\\\\\\\\\\\"}';
495
+ canSer(o, s);
496
+ });
497
+ it("should deserialize backslashes in string in object", () => {
498
+ const o: ObjWithString = { s: "\\\\\\" };
499
+ const s = '{"s":"\\\\\\\\\\\\"}';
500
+ canDeser(s, o);
501
+ });
502
+
503
+ it("should deserialize slashes in string in object", () => {
504
+ const o: ObjWithString = { s: "//" };
505
+ const s = '{"s":"/\\/"}';
506
+ canDeser(s, o);
507
+ });
508
+ it("should deserialize slashes in string in array", () => {
509
+ const a = ["/", "/"];
510
+ const s = '["/","\/"]';
511
+ canDeser(s, a);
512
+ });
513
+
514
+ it("should ser/de short escape sequences in strings in objects", () => {
515
+ const o: ObjWithString = { s: "\n\r\t\b\f" };
516
+ const s = '{"s":"\\n\\r\\t\\b\\f"}';
517
+ canSerde(o, s);
518
+ });
519
+
520
+ it("should ser/de short escape sequences in string arrays", () => {
521
+ const a = ["\n", "\r", "\t", "\b", "\f"];
522
+ const s = '["\\n","\\r","\\t","\\b","\\f"]';
523
+ canSerde(a, s);
524
+ });
525
+
526
+ it("should ser/de short escape sequences in string arrays in objects", () => {
527
+ const o: ObjectWithStringArray = { sa: ["\n", "\r", "\t", "\b", "\f"] };
528
+ const s = '{"sa":["\\n","\\r","\\t","\\b","\\f"]}';
529
+ canSerde(o, s);
530
+ });
531
+
532
+ it("should ser/de long escape sequences in strings in objects", () => {
533
+ const singles = ["\n", "\r", "\t", "\b", "\f"];
534
+ let x = "";
535
+ let y = "";
536
+ for (let i = 0; i < 0x1F; i++) {
537
+ const c = String.fromCharCode(i);
538
+ if (singles.includes(c)) continue;
539
+ x += c;
540
+ y += `\\u${i.toString(16).padStart(4, "0")}`;
541
+ }
542
+ const o: ObjWithString = { s: x };
543
+ const s = `{"s":"${y}"}`;
544
+ canSerde(o, s);
545
+ });
546
+
547
+ it("should ser/de long escape sequences in strings in arrays", () => {
548
+ const singles = ["\n", "\r", "\t", "\b", "\f"];
549
+ let x: string[] = [];
550
+ let y: string[] = [];
551
+ for (let i = 0; i < 0x1F; i++) {
552
+ const c = String.fromCharCode(i);
553
+ if (singles.includes(c)) continue;
554
+ x.push(c);
555
+ y.push(`\\u${i.toString(16).padStart(4, "0")}`);
556
+ }
557
+ const a = x;
558
+ const s = `["${y.join('","')}"]`;
559
+ canSerde(a, s);
560
+ });
561
+
562
+ it("should ser/de long escape sequences in string arrays in objects", () => {
563
+ const singles = ["\n", "\r", "\t", "\b", "\f"];
564
+ let x: string[] = [];
565
+ let y: string[] = [];
566
+ for (let i = 0; i < 0x1F; i++) {
567
+ const c = String.fromCharCode(i);
568
+ if (singles.includes(c)) continue;
569
+ x.push(c);
570
+ y.push(`\\u${i.toString(16).padStart(4, "0")}`);
571
+ }
572
+ const o: ObjectWithStringArray = { sa: x };
573
+ const s = `{"sa":["${y.join('","')}"]}`;
574
+ canSerde(o, s);
575
+ });
576
+
577
+ });
578
+
579
+ describe("Ser/de special strings in object keys", () => {
580
+
581
+ it("should ser/de escape sequences in key of object with int value", () => {
582
+ const o: ObjWithStrangeKey<i32> = { data: 123 };
583
+ const s = '{"a\\\\\\t\\"\\u0002b`c":123}';
584
+ canSerde(o, s);
585
+ });
586
+
587
+ it("should ser/de escape sequences in key of object with float value", () => {
588
+ const o: ObjWithStrangeKey<f64> = { data: 123.4 };
589
+ const s = '{"a\\\\\\t\\"\\u0002b`c":123.4}';
590
+ canSerde(o, s);
591
+ });
592
+
593
+ it("should ser/de escape sequences in key of object with string value", () => {
594
+ const o: ObjWithStrangeKey<string> = { data: "abc" };
595
+ const s = '{"a\\\\\\t\\"\\u0002b`c":"abc"}';
596
+ canSerde(o, s);
597
+ });
598
+
599
+ // Something buggy in as-pect needs a dummy value reflected here
600
+ // or the subsequent test fails. It's not used in any test.
601
+ Reflect.toReflectedValue(0);
602
+
603
+ it("should ser/de escape sequences in map key", () => {
604
+ const m = new Map<string, string>();
605
+ m.set('a\\\t"\x02b', 'abc');
606
+ const s = '{"a\\\\\\t\\"\\u0002b":"abc"}';
607
+ canSerde(m, s);
608
+ });
609
+ it("should ser/de escape sequences in map value", () => {
610
+ const m = new Map<string, string>();
611
+ m.set('abc', 'a\\\t"\x02b');
612
+ const s = '{"abc":"a\\\\\\t\\"\\u0002b"}';
613
+ canSerde(m, s);
614
+ });
615
+ });
616
+
617
+ @json
618
+ class ObjWithString {
619
+ s!: string;
620
+ }
621
+
622
+ @json
623
+ class ObjectWithStringArray {
624
+ sa!: string[];
625
+ }
626
+
627
+ @json
628
+ class ObjectWithFloat {
629
+ f!: f64;
630
+ }
631
+
632
+ @json
633
+ class ObjectWithFloatArray {
634
+ fa!: f64[];
635
+ }
636
+
637
+ @json
638
+ class ObjWithStrangeKey<T> {
639
+ @alias('a\\\t"\x02b`c')
640
+ data!: T;
641
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Class decorator that enables the class to be serializable as JSON.
3
+ */
4
+ declare function json(target: any): void;
5
+
6
+ /**
7
+ * Class decorator that enables the class to be serializable as JSON.
8
+ */
9
+ declare function serializable(target: any): void;
10
+
11
+ /**
12
+ * Property decorator that provides an alias name for JSON serialization.
13
+ */
14
+ declare function alias(name: string): Function;
package/assembly/index.ts CHANGED
@@ -1 +1,3 @@
1
+ /// <reference path="./index.d.ts" />
2
+
1
3
  export { JSON } from "./src/json";
@@ -35,6 +35,8 @@
35
35
  @inline export const sCode = 115;
36
36
  // @ts-ignore = Decorator is valid here
37
37
  @inline export const nCode = 110;
38
+ // @ts-ignore = Decorator is valid here
39
+ @inline export const bCode = 98;
38
40
  // Strings
39
41
  // @ts-ignore: Decorator is valid here
40
42
  @inline export const trueWord = "true";
@@ -58,6 +60,15 @@
58
60
  @inline export const rightBracketWord = "]";
59
61
  // @ts-ignore: Decorator is valid here
60
62
  @inline export const quoteWord = "\"";
63
+
61
64
  // Escape Codes
62
65
  // @ts-ignore: Decorator is valid here
63
- @inline export const newLineCode = 10;
66
+ @inline export const backspaceCode = 8; // \b
67
+ // @ts-ignore: Decorator is valid here
68
+ @inline export const tabCode = 9; // \t
69
+ // @ts-ignore: Decorator is valid here
70
+ @inline export const newLineCode = 10; // \n
71
+ // @ts-ignore: Decorator is valid here
72
+ @inline export const formFeedCode = 12; // \f
73
+ // @ts-ignore: Decorator is valid here
74
+ @inline export const carriageReturnCode = 13; // \r
@@ -2,6 +2,7 @@ import { StringSink } from "as-string-sink/assembly";
2
2
  import { isSpace } from "util/string";
3
3
  import {
4
4
  aCode,
5
+ bCode,
5
6
  eCode,
6
7
  fCode,
7
8
  lCode,
@@ -14,20 +15,24 @@ import {
14
15
  backSlashCode,
15
16
  colonCode,
16
17
  commaCode,
18
+ forwardSlashCode,
17
19
  leftBraceCode,
18
20
  leftBracketCode,
19
- newLineCode,
20
21
  quoteCode,
21
22
  rightBraceCode,
22
23
  rightBracketCode,
23
24
 
24
- colonWord,
25
+ backspaceCode,
26
+ carriageReturnCode,
27
+ tabCode,
28
+ formFeedCode,
29
+ newLineCode,
30
+
25
31
  commaWord,
26
32
  quoteWord,
27
33
 
28
34
  leftBraceWord,
29
35
  leftBracketWord,
30
- rightBraceWord,
31
36
  rightBracketWord,
32
37
  emptyArrayWord,
33
38
 
@@ -35,7 +40,7 @@ import {
35
40
  falseWord,
36
41
  nullWord,
37
42
  } from "./chars";
38
- import { snip_fast, unsafeCharCodeAt } from "./util";
43
+ import { snip_fast, unsafeCharCodeAt, containsCodePoint } from "./util";
39
44
  import { Virtual } from "as-virtual/assembly";
40
45
 
41
46
  /**
@@ -68,7 +73,7 @@ export namespace JSON {
68
73
  // @ts-ignore: Hidden function
69
74
  return data.__JSON_Serialize();
70
75
  } else if (data instanceof Date) {
71
- return "\"" + data.toISOString() + "\"";
76
+ return `"${data.toISOString()}"`;
72
77
  } else if (isArrayLike<T>()) {
73
78
  // @ts-ignore
74
79
  if (data.length == 0) {
@@ -100,11 +105,11 @@ export namespace JSON {
100
105
  for (let i = 0; i < data.length - 1; i++) {
101
106
  // @ts-ignore
102
107
  result.write(JSON.stringify(unchecked(data[i])));
103
- result.write(commaWord);
108
+ result.writeCodePoint(commaCode);
104
109
  }
105
110
  // @ts-ignore
106
111
  result.write(JSON.stringify(unchecked(data[data.length - 1])));
107
- result.write(rightBracketWord);
112
+ result.writeCodePoint(rightBracketCode);
108
113
  return result.toString();
109
114
  }
110
115
  } else if (data instanceof Map) {
@@ -112,14 +117,14 @@ export namespace JSON {
112
117
  let keys = data.keys();
113
118
  let values = data.values();
114
119
  for (let i = 0; i < data.size; i++) {
115
- result.write(serializeString(keys[i].toString()));
116
- result.write(colonWord);
117
- result.write(JSON.stringify(values[i]));
120
+ result.write(serializeString(unchecked(keys[i]).toString()));
121
+ result.writeCodePoint(colonCode);
122
+ result.write(JSON.stringify(unchecked(values[i])));
118
123
  if (i < data.size - 1) {
119
- result.write(commaWord);
124
+ result.writeCodePoint(commaCode);
120
125
  }
121
126
  }
122
- result.write(rightBraceWord);
127
+ result.writeCodePoint(rightBraceCode);
123
128
  return result.toString();
124
129
  } else {
125
130
  throw new Error(
@@ -194,11 +199,11 @@ export namespace JSON {
194
199
  for (let i = 0; i < data.length - 1; i++) {
195
200
  // @ts-ignore
196
201
  result.write(JSON.stringify(unchecked(data[i])));
197
- result.write(commaWord);
202
+ result.writeCodePoint(commaCode);
198
203
  }
199
204
  // @ts-ignore
200
205
  result.write(JSON.stringify(unchecked(data[data.length - 1])));
201
- result.write(rightBracketWord);
206
+ result.writeCodePoint(rightBracketCode);
202
207
  out = result.toString();
203
208
  return;
204
209
  }
@@ -254,51 +259,8 @@ export namespace JSON {
254
259
  @global @inline function __parseObjectValue<T>(data: string, initializeDefaultValues: boolean): T {
255
260
  let type: T;
256
261
  if (isString<T>()) {
257
- let result = "";
258
- let last = 0;
259
- for (let i = 0; i < data.length; i++) {
260
- // \\"
261
- if (unsafeCharCodeAt(data, i) === backSlashCode) {
262
- const char = unsafeCharCodeAt(data, ++i);
263
- result += data.slice(last, i - 1);
264
- if (char === 34) {
265
- result += '"';
266
- last = ++i;
267
- } else if (char === 110) {
268
- result += "\n";
269
- last = ++i;
270
- // 92 98 114 116 102 117
271
- } else if (char >= 92 && char <= 117) {
272
- if (char === 92) {
273
- result += "\\";
274
- last = ++i;
275
- } else if (char === 98) {
276
- result += "\b";
277
- last = ++i;
278
- } else if (char === 102) {
279
- result += "\f";
280
- last = ++i;
281
- } else if (char === 114) {
282
- result += "\r";
283
- last = ++i;
284
- } else if (char === 116) {
285
- result += "\t";
286
- last = ++i;
287
- } else if (
288
- char === 117 &&
289
- load<u64>(changetype<usize>(data) + <usize>((i + 1) << 1)) ===
290
- 27584753879220272
291
- ) {
292
- result += "\u000b";
293
- i += 4;
294
- last = ++i;
295
- }
296
- }
297
- }
298
- }
299
- result += data.slice(last);
300
262
  // @ts-ignore
301
- return result;
263
+ return data;
302
264
  } else if (isBoolean<T>()) {
303
265
  // @ts-ignore
304
266
  return parseBoolean<T>(data);
@@ -327,118 +289,133 @@ export namespace JSON {
327
289
 
328
290
  // @ts-ignore: Decorator
329
291
  @inline function serializeString(data: string): string {
330
- let result = new StringSink('"');
292
+ if (data.length === 0) {
293
+ return quoteWord + quoteWord;
294
+ }
295
+
296
+ let result = new StringSink(quoteWord);
331
297
 
332
298
  let last: i32 = 0;
333
299
  for (let i = 0; i < data.length; i++) {
334
300
  const char = unsafeCharCodeAt(<string>data, i);
335
- if (char === 34 || char === 92) {
301
+ if (char === quoteCode || char === backSlashCode) {
336
302
  result.write(<string>data, last, i);
337
303
  result.writeCodePoint(backSlashCode);
338
304
  last = i;
339
- } else if (char <= 13 && char >= 8) {
305
+ } else if (char < 16) {
340
306
  result.write(<string>data, last, i);
341
307
  last = i + 1;
342
308
  switch (char) {
343
- case 8: {
309
+ case backspaceCode: {
344
310
  result.write("\\b");
345
311
  break;
346
312
  }
347
- case 9: {
313
+ case tabCode: {
348
314
  result.write("\\t");
349
315
  break;
350
316
  }
351
- case 10: {
317
+ case newLineCode: {
352
318
  result.write("\\n");
353
319
  break;
354
320
  }
355
- case 11: {
356
- result.write("\\x0B"); // \\u000b
357
- break;
358
- }
359
- case 12: {
321
+ case formFeedCode: {
360
322
  result.write("\\f");
361
323
  break;
362
324
  }
363
- case 13: {
325
+ case carriageReturnCode: {
364
326
  result.write("\\r");
365
327
  break;
366
328
  }
329
+ default: {
330
+ // all chars 0-31 must be encoded as a four digit unicode escape sequence
331
+ // \u0000 to \u000f handled here
332
+ result.write("\\u000");
333
+ result.write(char.toString(16));
334
+ break;
335
+ }
367
336
  }
337
+ } else if (char < 32) {
338
+ result.write(<string>data, last, i);
339
+ last = i + 1;
340
+ // all chars 0-31 must be encoded as a four digit unicode escape sequence
341
+ // \u0010 to \u001f handled here
342
+ result.write("\\u00");
343
+ result.write(char.toString(16));
368
344
  }
369
345
  }
370
- if (result.length === 1) {
371
- return quoteWord + data + quoteWord;
372
- }
373
346
  result.write(<string>data, last);
374
- result.write(quoteWord);
347
+ result.writeCodePoint(quoteCode);
375
348
  return result.toString();
376
349
  }
377
350
 
378
351
  // @ts-ignore: Decorator
379
- @inline function parseString(data: string): string {
380
- let result = new StringSink();
381
- let last = 1;
382
- for (let i = 1; i < data.length - 1; i++) {
383
- // \\"
384
- if (unsafeCharCodeAt(data, i) === backSlashCode) {
385
- const char = unsafeCharCodeAt(data, ++i);
386
- result.write(data, last, i - 1);
387
- if (char === 34) {
352
+ @inline function parseString(data: string, start: i32 = 0, end: i32 = 0): string {
353
+ end = end || data.length - 1;
354
+ let result = StringSink.withCapacity(end - start - 1);
355
+ let last = start + 1;
356
+ for (let i = last; i < end; i++) {
357
+ if (unsafeCharCodeAt(data, i) !== backSlashCode) {
358
+ continue;
359
+ }
360
+ const char = unsafeCharCodeAt(data, ++i);
361
+ result.write(data, last, i - 1);
362
+ switch (char) {
363
+ case quoteCode: {
388
364
  result.writeCodePoint(quoteCode);
389
365
  last = i + 1;
390
- } else if (char >= 92 && char <= 117) {
391
- switch (char) {
392
- case 92: {
393
- result.writeCodePoint(backSlashCode);
394
- last = i + 1;
395
- break;
396
- }
397
- case 98: {
398
- result.write("\b");
399
- last = i + 1;
400
- break;
401
- }
402
- case 102: {
403
- result.write("\f");
404
- last = i + 1;
405
- break;
406
- }
407
- case 110: {
408
- result.writeCodePoint(newLineCode);
409
- last = i + 1;
410
- break;
411
- }
412
- case 114: {
413
- result.write("\r");
414
- last = i + 1;
415
- break;
416
- }
417
- case 116: {
418
- result.write("\t");
419
- last = i + 1;
420
- break;
421
- }
422
- default: {
423
- if (
424
- char === 117 &&
425
- load<u64>(changetype<usize>(data) + <usize>((i + 1) << 1)) ===
426
- 27584753879220272
427
- ) {
428
- result.write("\u000b");
429
- i += 4;
430
- last = i + 1;
431
- }
432
- break;
433
- }
434
- }
366
+ break;
367
+ }
368
+ case backSlashCode: {
369
+ result.writeCodePoint(backSlashCode);
370
+ last = i + 1;
371
+ break;
372
+ }
373
+ case forwardSlashCode: {
374
+ result.writeCodePoint(forwardSlashCode);
375
+ last = i + 1;
376
+ break;
377
+ }
378
+ case bCode: {
379
+ result.writeCodePoint(backspaceCode);
380
+ last = i + 1;
381
+ break;
382
+ }
383
+ case fCode: {
384
+ result.writeCodePoint(formFeedCode);
385
+ last = i + 1;
386
+ break;
387
+ }
388
+ case nCode: {
389
+ result.writeCodePoint(newLineCode);
390
+ last = i + 1;
391
+ break;
392
+ }
393
+ case rCode: {
394
+ result.writeCodePoint(carriageReturnCode);
395
+ last = i + 1;
396
+ break;
397
+ }
398
+ case tCode: {
399
+ result.writeCodePoint(tabCode);
400
+ last = i + 1;
401
+ break;
402
+ }
403
+ case uCode: {
404
+ const code = u16.parse(data.slice(i + 1, i + 5), 16);
405
+ result.writeCodePoint(code);
406
+ i += 4;
407
+ last = i + 1;
408
+ break;
409
+ }
410
+ default: {
411
+ throw new Error(`JSON: Cannot parse "${data}" as string. Invalid escape sequence: \\${data.charAt(i)}`);
435
412
  }
436
413
  }
437
414
  }
438
- if ((data.length - 1) > last) {
439
- result.write(data, last, data.length - 1);
415
+ if (end > last) {
416
+ result.write(data, last, end);
440
417
  }
441
- return result.toString();
418
+ return result.toString()
442
419
  }
443
420
 
444
421
  // @ts-ignore: Decorator
@@ -491,7 +468,7 @@ export namespace JSON {
491
468
  if (depth === 0) {
492
469
  ++arrayValueIndex;
493
470
  // @ts-ignore
494
- schema.__JSON_Set_Key<Virtual<string>>(key, data, outerLoopIndex, arrayValueIndex, initializeDefaultValues);
471
+ schema.__JSON_Set_Key(key, data, outerLoopIndex, arrayValueIndex, initializeDefaultValues);
495
472
  outerLoopIndex = arrayValueIndex;
496
473
  isKey = false;
497
474
  break;
@@ -512,7 +489,7 @@ export namespace JSON {
512
489
  if (depth === 0) {
513
490
  ++objectValueIndex;
514
491
  // @ts-ignore
515
- schema.__JSON_Set_Key<Virtual<string>>(key, data, outerLoopIndex, objectValueIndex, initializeDefaultValues);
492
+ schema.__JSON_Set_Key(key, data, outerLoopIndex, objectValueIndex, initializeDefaultValues);
516
493
  outerLoopIndex = objectValueIndex;
517
494
  isKey = false;
518
495
  break;
@@ -530,15 +507,19 @@ export namespace JSON {
530
507
  if (char === backSlashCode && !escaping) {
531
508
  escaping = true;
532
509
  } else {
533
- if (
534
- char === quoteCode && !escaping
535
- ) {
510
+ if (char === quoteCode && !escaping) {
536
511
  if (isKey === false) {
537
- key.reinst(data, outerLoopIndex, stringValueIndex);
512
+ // perf: we can avoid creating a new string here if the key doesn't contain any escape sequences
513
+ if (containsCodePoint(data, backSlashCode, outerLoopIndex, stringValueIndex)) {
514
+ key.reinst(parseString(data, outerLoopIndex - 1, stringValueIndex));
515
+ } else {
516
+ key.reinst(data, outerLoopIndex, stringValueIndex);
517
+ }
538
518
  isKey = true;
539
519
  } else {
520
+ const value = parseString(data, outerLoopIndex - 1, stringValueIndex);
540
521
  // @ts-ignore
541
- schema.__JSON_Set_Key<Virtual<string>>(key, data, outerLoopIndex, stringValueIndex, initializeDefaultValues);
522
+ schema.__JSON_Set_Key(key, value, 0, value.length, initializeDefaultValues);
542
523
  isKey = false;
543
524
  }
544
525
  outerLoopIndex = ++stringValueIndex;
@@ -554,7 +535,7 @@ export namespace JSON {
554
535
  unsafeCharCodeAt(data, ++outerLoopIndex) === lCode
555
536
  ) {
556
537
  // @ts-ignore
557
- schema.__JSON_Set_Key<Virtual<string>>(key, nullWord, 0, 4, initializeDefaultValues);
538
+ schema.__JSON_Set_Key(key, nullWord, 0, 4, initializeDefaultValues);
558
539
  isKey = false;
559
540
  } else if (
560
541
  char === tCode &&
@@ -563,7 +544,7 @@ export namespace JSON {
563
544
  unsafeCharCodeAt(data, ++outerLoopIndex) === eCode
564
545
  ) {
565
546
  // @ts-ignore
566
- schema.__JSON_Set_Key<Virtual<string>>(key, trueWord, 0, 4, initializeDefaultValues);
547
+ schema.__JSON_Set_Key(key, trueWord, 0, 4, initializeDefaultValues);
567
548
  isKey = false;
568
549
  } else if (
569
550
  char === fCode &&
@@ -573,7 +554,7 @@ export namespace JSON {
573
554
  unsafeCharCodeAt(data, ++outerLoopIndex) === eCode
574
555
  ) {
575
556
  // @ts-ignore
576
- schema.__JSON_Set_Key<Virtual<string>>(key, falseWord, 0, 5, initializeDefaultValues);
557
+ schema.__JSON_Set_Key(key, falseWord, 0, 5, initializeDefaultValues);
577
558
  isKey = false;
578
559
  } else if ((char >= 48 && char <= 57) || char === 45) {
579
560
  let numberValueIndex = ++outerLoopIndex;
@@ -581,7 +562,7 @@ export namespace JSON {
581
562
  const char = unsafeCharCodeAt(data, numberValueIndex);
582
563
  if (char === commaCode || char === rightBraceCode || isSpace(char)) {
583
564
  // @ts-ignore
584
- schema.__JSON_Set_Key<Virtual<string>>(key, data, outerLoopIndex - 1, numberValueIndex, initializeDefaultValues);
565
+ schema.__JSON_Set_Key(key, data, outerLoopIndex - 1, numberValueIndex, initializeDefaultValues);
585
566
  outerLoopIndex = numberValueIndex;
586
567
  isKey = false;
587
568
  break;
@@ -664,11 +645,17 @@ export namespace JSON {
664
645
  char === quoteCode && !escaping
665
646
  ) {
666
647
  if (isKey === false) {
667
- key.reinst(data, outerLoopIndex, stringValueIndex);
648
+ // perf: we can avoid creating a new string here if the key doesn't contain any escape sequences
649
+ if (containsCodePoint(data, backSlashCode, outerLoopIndex, stringValueIndex)) {
650
+ key.reinst(parseString(data, outerLoopIndex - 1, stringValueIndex));
651
+ } else {
652
+ key.reinst(data, outerLoopIndex, stringValueIndex);
653
+ }
668
654
  isKey = true;
669
655
  } else {
670
656
  if (isString<valueof<T>>()) {
671
- map.set(parseMapKey<indexof<T>>(key), data.slice(outerLoopIndex, stringValueIndex));
657
+ const value = parseString(data, outerLoopIndex - 1, stringValueIndex);
658
+ map.set(parseMapKey<indexof<T>>(key), value);
672
659
  }
673
660
  isKey = false;
674
661
  }
@@ -788,7 +775,7 @@ export namespace JSON {
788
775
  lastPos = i;
789
776
  } else {
790
777
  instr = false;
791
- result.push(parseString(data.slice(lastPos, i + 1)));
778
+ result.push(parseString(data, lastPos, i));
792
779
  }
793
780
  }
794
781
  escaping = false;
@@ -803,16 +790,6 @@ export namespace JSON {
803
790
  let lastPos = 1;
804
791
  for (let i = 1; i < data.length - 1; i++) {
805
792
  const char = unsafeCharCodeAt(data, i);
806
- /*// if char == "t" && i+3 == "e"
807
- if (char === tCode && data.charCodeAt(i + 3) === eCode) {
808
- //i += 3;
809
- result.push(parseBoolean<valueof<T>>(data.slice(lastPos, i+2)));
810
- //i++;
811
- } else if (char === fCode && data.charCodeAt(i + 4) === eCode) {
812
- //i += 4;
813
- result.push(parseBoolean<valueof<T>>(data.slice(lastPos, i+3)));
814
- //i++;
815
- }*/
816
793
  if (char === tCode || char === fCode) {
817
794
  lastPos = i;
818
795
  } else if (char === eCode) {
@@ -830,7 +807,7 @@ export namespace JSON {
830
807
  let i = 1;
831
808
  for (; i < data.length - 1; i++) {
832
809
  const char = unsafeCharCodeAt(data, i);
833
- if ((lastPos === 0 && char >= 48 && char <= 57) || char === 45) {
810
+ if (lastPos === 0 && ((char >= 48 && char <= 57) || char === 45)) {
834
811
  lastPos = i;
835
812
  } else if ((isSpace(char) || char == commaCode) && lastPos > 0) {
836
813
  result.push(parseNumber<valueof<T>>(data.slice(lastPos, i)));
@@ -12,11 +12,11 @@ import { backSlashCode, quoteCode } from "./chars";
12
12
  const result = new StringSink();
13
13
  let instr = false;
14
14
  for (let i = 0; i < data.length; i++) {
15
- const char = data.charCodeAt(i);
15
+ const char = unsafeCharCodeAt(data, i);
16
16
  if (instr === false && char === quoteCode) instr = true;
17
17
  else if (
18
18
  instr === true && char === quoteCode
19
- && data.charCodeAt(i - 1) !== backSlashCode
19
+ && unsafeCharCodeAt(data, i - 1) !== backSlashCode
20
20
  ) instr = false;
21
21
 
22
22
  if (instr === false) {
@@ -347,4 +347,12 @@ import { backSlashCode, quoteCode } from "./chars";
347
347
  return load<u16>(changetype<usize>(p1_data) + p1_start) == load<u16>(changetype<usize>(p2_data) + p2_start)
348
348
  }
349
349
  return memory.compare(changetype<usize>(p1_data) + p1_start, changetype<usize>(p2_data) + p2_start, p1_len) === 0;
350
- }
350
+ }
351
+
352
+ // @ts-ignore
353
+ @inline export function containsCodePoint(str: string, code: u32, start: i32, end: i32): bool {
354
+ for (let i = start; i <= end; i++) {
355
+ if (unsafeCharCodeAt(str, i) == code) return true;
356
+ }
357
+ return false;
358
+ }
package/assembly/test.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { JSON } from "./src/json";
2
2
 
3
- // @ts-ignore
4
3
  @serializable
5
4
  class Vec3 {
6
5
  x: f64 = 3.4;
@@ -8,7 +7,6 @@ class Vec3 {
8
7
  z: f64 = 8.3;
9
8
  }
10
9
 
11
- // @ts-ignore
12
10
  @serializable
13
11
  class Player extends Vec3 {
14
12
  @alias("first name")
@@ -57,7 +55,7 @@ class Wrapper<T> {
57
55
 
58
56
  @serializable
59
57
  class Foo {
60
- @alias("ur mom")
58
+ @alias("hello")
61
59
  foo!: string;
62
60
  }
63
61
 
@@ -72,7 +70,7 @@ const foo: Wrapper<Foo> = {
72
70
 
73
71
  foo.data.foo = "ha";
74
72
  console.log(JSON.stringify(foo));
75
- console.log(JSON.stringify(JSON.parse<Wrapper<Foo>>("{\"data\":{\"ur mom\":\"ha\"}}")))
73
+ console.log(JSON.stringify(JSON.parse<Wrapper<Foo>>("{\"data\":{\"hello\":\"ha\"}}")))
76
74
  /*
77
75
  // 9,325,755
78
76
  bench("Stringify Object (Vec3)", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-as",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "JSON encoder/decoder for AssemblyScript",
5
5
  "types": "assembly/index.ts",
6
6
  "author": "Jairus Tanaka",
@@ -1,3 +1,4 @@
1
+ import { Parser, Source, Tokenizer, } from "assemblyscript/dist/assemblyscript.js";
1
2
  import { toString, isStdlib } from "visitor-as/dist/utils.js";
2
3
  import { BaseVisitor, SimpleParser } from "visitor-as/dist/index.js";
3
4
  import { Transform } from "assemblyscript/dist/transform.js";
@@ -17,7 +18,7 @@ class AsJSONTransform extends BaseVisitor {
17
18
  constructor() {
18
19
  super(...arguments);
19
20
  this.schemasList = [];
20
- this.sources = [];
21
+ this.sources = new Set();
21
22
  }
22
23
  visitMethodDeclaration() { }
23
24
  visitClassDeclaration(node) {
@@ -77,8 +78,10 @@ class AsJSONTransform extends BaseVisitor {
77
78
  let type = toString(member.type);
78
79
  const name = member.name.text;
79
80
  let aliasName = name;
81
+ // @ts-ignore
80
82
  if (member.decorators && ((_d = member.decorators[0]) === null || _d === void 0 ? void 0 : _d.name.text) === "alias") {
81
83
  if (member.decorators[0] && member.decorators[0].args[0]) {
84
+ // @ts-ignore
82
85
  aliasName = member.decorators[0].args[0].value;
83
86
  }
84
87
  }
@@ -96,13 +99,12 @@ class AsJSONTransform extends BaseVisitor {
96
99
  "u64",
97
100
  "i64",
98
101
  ].includes(type.toLowerCase())) {
99
- this.currentClass.encodeStmts.push(`"${aliasName}":\${this.${name}.toString()},`);
102
+ this.currentClass.encodeStmts.push(`${encodeKey(aliasName)}:\${this.${name}},`);
100
103
  // @ts-ignore
101
- this.currentClass.setDataStmts.push(`if (key.equals("${aliasName}")) {
102
- this.${name} = __atoi_fast<${type}>(data, val_start << 1, val_end << 1);
103
- return;
104
- }
105
- `);
104
+ this.currentClass.setDataStmts.push(`if (key.equals(${JSON.stringify(aliasName)})) {
105
+ this.${name} = __atoi_fast<${type}>(data, val_start << 1, val_end << 1);
106
+ return;
107
+ }`);
106
108
  if (member.initializer) {
107
109
  this.currentClass.initializeStmts.push(`this.${name} = ${toString(member.initializer)}`);
108
110
  }
@@ -112,25 +114,23 @@ class AsJSONTransform extends BaseVisitor {
112
114
  "f32",
113
115
  "f64",
114
116
  ].includes(type.toLowerCase())) {
115
- this.currentClass.encodeStmts.push(`"${aliasName}":\${this.${name}.toString()},`);
117
+ this.currentClass.encodeStmts.push(`${encodeKey(aliasName)}:\${this.${name}},`);
116
118
  // @ts-ignore
117
- this.currentClass.setDataStmts.push(`if (key.equals("${aliasName}")) {
118
- this.${name} = __parseObjectValue<${type}>(data.slice(val_start, val_end), initializeDefaultValues);
119
- return;
120
- }
121
- `);
119
+ this.currentClass.setDataStmts.push(`if (key.equals(${JSON.stringify(aliasName)})) {
120
+ this.${name} = __parseObjectValue<${type}>(data.slice(val_start, val_end), initializeDefaultValues);
121
+ return;
122
+ }`);
122
123
  if (member.initializer) {
123
124
  this.currentClass.initializeStmts.push(`this.${name} = ${toString(member.initializer)}`);
124
125
  }
125
126
  }
126
127
  else {
127
- this.currentClass.encodeStmts.push(`"${aliasName}":\${JSON.stringify<${type}>(this.${name})},`);
128
+ this.currentClass.encodeStmts.push(`${encodeKey(aliasName)}:\${JSON.stringify<${type}>(this.${name})},`);
128
129
  // @ts-ignore
129
- this.currentClass.setDataStmts.push(`if (key.equals("${aliasName}")) {
130
- this.${name} = __parseObjectValue<${type}>(val_start ? data.slice(val_start, val_end) : data, initializeDefaultValues);
131
- return;
132
- }
133
- `);
130
+ this.currentClass.setDataStmts.push(`if (key.equals(${JSON.stringify(aliasName)})) {
131
+ this.${name} = __parseObjectValue<${type}>(val_start ? data.slice(val_start, val_end) : data, initializeDefaultValues);
132
+ return;
133
+ }`);
134
134
  if (member.initializer) {
135
135
  this.currentClass.initializeStmts.push(`this.${name} = ${toString(member.initializer)}`);
136
136
  }
@@ -143,38 +143,32 @@ class AsJSONTransform extends BaseVisitor {
143
143
  this.currentClass.encodeStmts[this.currentClass.encodeStmts.length - 1] =
144
144
  stmt.slice(0, stmt.length - 1);
145
145
  serializeFunc = `
146
- @inline __JSON_Serialize(): string {
146
+ __JSON_Serialize(): string {
147
147
  return \`{${this.currentClass.encodeStmts.join("")}}\`;
148
- }
149
- `;
148
+ }`;
150
149
  }
151
150
  else {
152
151
  serializeFunc = `
153
- @inline __JSON_Serialize(): string {
152
+ __JSON_Serialize(): string {
154
153
  return "{}";
155
- }
156
- `;
154
+ }`;
157
155
  }
158
- // Odd behavior here... When pairing this transform with asyncify, having @inline on __JSON_Set_Key<T> with a generic will cause it to freeze.
159
- // Binaryen cannot predict and add/mangle code when it is genericed.
160
156
  const setKeyFunc = `
161
- __JSON_Set_Key<__JSON_Key_Type>(key: __JSON_Key_Type, data: string, val_start: i32, val_end: i32, initializeDefaultValues: boolean): void {
162
- ${
163
- // @ts-ignore
164
- this.currentClass.setDataStmts.join("")}
157
+ __JSON_Set_Key(key: __Virtual<string>, data: string, val_start: i32, val_end: i32, initializeDefaultValues: boolean): void {
158
+ ${this.currentClass.setDataStmts.join("\n ")}
165
159
  }
166
160
  `;
167
161
  let initializeFunc = "";
168
162
  if (this.currentClass.initializeStmts.length > 0) {
169
163
  initializeFunc = `
170
- @inline __JSON_Initialize(): void {
164
+ __JSON_Initialize(): void {
171
165
  ${this.currentClass.initializeStmts.join(";\n")};
172
166
  }
173
167
  `;
174
168
  }
175
169
  else {
176
170
  initializeFunc = `
177
- @inline __JSON_Initialize(): void {}
171
+ __JSON_Initialize(): void {}
178
172
  `;
179
173
  }
180
174
  const serializeMethod = SimpleParser.parseClassMember(serializeFunc, node);
@@ -184,12 +178,37 @@ class AsJSONTransform extends BaseVisitor {
184
178
  const initializeMethod = SimpleParser.parseClassMember(initializeFunc, node);
185
179
  node.members.push(initializeMethod);
186
180
  this.schemasList.push(this.currentClass);
187
- //console.log(toString(node));
181
+ this.sources.add(node.name.range.source);
182
+ // Uncomment to see the generated code for debugging.
183
+ // console.log(serializeFunc);
184
+ // console.log(setKeyFunc);
185
+ // console.log(initializeFunc);
188
186
  }
189
187
  visitSource(node) {
190
188
  super.visitSource(node);
189
+ // Only add the import statement to sources that have JSON decorated classes.
190
+ if (!this.sources.has(node)) {
191
+ return;
192
+ }
193
+ // Note, the following one liner would be easier, but it fails with an assertion error
194
+ // because as-virtual's SimpleParser doesn't set the parser.currentSource correctly.
195
+ //
196
+ // const stmt = SimpleParser.parseTopLevelStatement('import { Virtual as __Virtual } from "as-virtual/assembly";');
197
+ // ... So we have to do it the long way:
198
+ const s = 'import { Virtual as __Virtual } from "as-virtual/assembly";';
199
+ const t = new Tokenizer(new Source(0 /* SourceKind.User */, "index.ts", s));
200
+ const p = new Parser();
201
+ p.currentSource = t.source;
202
+ const stmt = p.parseTopLevelStatement(t);
203
+ // Add the import statement to the top of the source.
204
+ node.statements.unshift(stmt);
191
205
  }
192
206
  }
207
+ function encodeKey(aliasName) {
208
+ return JSON.stringify(aliasName)
209
+ .replace(/\\/g, "\\\\")
210
+ .replace(/\`/g, '\\`');
211
+ }
193
212
  export default class Transformer extends Transform {
194
213
  // Trigger the transform after parse.
195
214
  afterParse(parser) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@json-as/transform",
3
- "version": "0.7.0",
3
+ "version": "0.7.3",
4
4
  "description": "JSON encoder/decoder for AssemblyScript",
5
5
  "main": "./lib/index.js",
6
6
  "author": "Jairus Tanaka",
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  ClassDeclaration,
3
3
  FieldDeclaration,
4
- Source,
5
4
  Parser,
6
- } from "assemblyscript/dist/assemblyscript";
5
+ Source,
6
+ SourceKind,
7
+ Tokenizer,
8
+ } from "assemblyscript/dist/assemblyscript.js";
9
+
7
10
  import { toString, isStdlib } from "visitor-as/dist/utils.js";
8
11
  import { BaseVisitor, SimpleParser } from "visitor-as/dist/index.js";
9
12
  import { Transform } from "assemblyscript/dist/transform.js";
@@ -23,7 +26,7 @@ class SchemaData {
23
26
  class AsJSONTransform extends BaseVisitor {
24
27
  public schemasList: SchemaData[] = [];
25
28
  public currentClass!: SchemaData;
26
- public sources: Source[] = [];
29
+ public sources = new Set<Source>();
27
30
 
28
31
  visitMethodDeclaration(): void { }
29
32
  visitClassDeclaration(node: ClassDeclaration): void {
@@ -89,8 +92,11 @@ class AsJSONTransform extends BaseVisitor {
89
92
 
90
93
  const name = member.name.text;
91
94
  let aliasName = name;
95
+
96
+ // @ts-ignore
92
97
  if (member.decorators && member.decorators[0]?.name.text === "alias") {
93
98
  if (member.decorators[0] && member.decorators[0].args![0]) {
99
+ // @ts-ignore
94
100
  aliasName = member.decorators[0].args![0].value;
95
101
  }
96
102
  }
@@ -111,15 +117,14 @@ class AsJSONTransform extends BaseVisitor {
111
117
  ].includes(type.toLowerCase())
112
118
  ) {
113
119
  this.currentClass.encodeStmts.push(
114
- `"${aliasName}":\${this.${name}.toString()},`
120
+ `${encodeKey(aliasName)}:\${this.${name}},`
115
121
  );
116
122
  // @ts-ignore
117
123
  this.currentClass.setDataStmts.push(
118
- `if (key.equals("${aliasName}")) {
119
- this.${name} = __atoi_fast<${type}>(data, val_start << 1, val_end << 1);
120
- return;
121
- }
122
- `
124
+ `if (key.equals(${JSON.stringify(aliasName)})) {
125
+ this.${name} = __atoi_fast<${type}>(data, val_start << 1, val_end << 1);
126
+ return;
127
+ }`
123
128
  );
124
129
  if (member.initializer) {
125
130
  this.currentClass.initializeStmts.push(
@@ -134,15 +139,14 @@ class AsJSONTransform extends BaseVisitor {
134
139
  ].includes(type.toLowerCase())
135
140
  ) {
136
141
  this.currentClass.encodeStmts.push(
137
- `"${aliasName}":\${this.${name}.toString()},`
142
+ `${encodeKey(aliasName)}:\${this.${name}},`
138
143
  );
139
144
  // @ts-ignore
140
145
  this.currentClass.setDataStmts.push(
141
- `if (key.equals("${aliasName}")) {
142
- this.${name} = __parseObjectValue<${type}>(data.slice(val_start, val_end), initializeDefaultValues);
143
- return;
144
- }
145
- `
146
+ `if (key.equals(${JSON.stringify(aliasName)})) {
147
+ this.${name} = __parseObjectValue<${type}>(data.slice(val_start, val_end), initializeDefaultValues);
148
+ return;
149
+ }`
146
150
  );
147
151
  if (member.initializer) {
148
152
  this.currentClass.initializeStmts.push(
@@ -151,15 +155,14 @@ class AsJSONTransform extends BaseVisitor {
151
155
  }
152
156
  } else {
153
157
  this.currentClass.encodeStmts.push(
154
- `"${aliasName}":\${JSON.stringify<${type}>(this.${name})},`
158
+ `${encodeKey(aliasName)}:\${JSON.stringify<${type}>(this.${name})},`
155
159
  );
156
160
  // @ts-ignore
157
161
  this.currentClass.setDataStmts.push(
158
- `if (key.equals("${aliasName}")) {
159
- this.${name} = __parseObjectValue<${type}>(val_start ? data.slice(val_start, val_end) : data, initializeDefaultValues);
160
- return;
161
- }
162
- `
162
+ `if (key.equals(${JSON.stringify(aliasName)})) {
163
+ this.${name} = __parseObjectValue<${type}>(val_start ? data.slice(val_start, val_end) : data, initializeDefaultValues);
164
+ return;
165
+ }`
163
166
  );
164
167
  if (member.initializer) {
165
168
  this.currentClass.initializeStmts.push(
@@ -180,26 +183,19 @@ class AsJSONTransform extends BaseVisitor {
180
183
  this.currentClass.encodeStmts[this.currentClass.encodeStmts.length - 1] =
181
184
  stmt!.slice(0, stmt.length - 1);
182
185
  serializeFunc = `
183
- @inline __JSON_Serialize(): string {
186
+ __JSON_Serialize(): string {
184
187
  return \`{${this.currentClass.encodeStmts.join("")}}\`;
185
- }
186
- `;
188
+ }`;
187
189
  } else {
188
190
  serializeFunc = `
189
- @inline __JSON_Serialize(): string {
191
+ __JSON_Serialize(): string {
190
192
  return "{}";
191
- }
192
- `;
193
+ }`;
193
194
  }
194
195
 
195
- // Odd behavior here... When pairing this transform with asyncify, having @inline on __JSON_Set_Key<T> with a generic will cause it to freeze.
196
- // Binaryen cannot predict and add/mangle code when it is genericed.
197
196
  const setKeyFunc = `
198
- __JSON_Set_Key<__JSON_Key_Type>(key: __JSON_Key_Type, data: string, val_start: i32, val_end: i32, initializeDefaultValues: boolean): void {
199
- ${
200
- // @ts-ignore
201
- this.currentClass.setDataStmts.join("")
202
- }
197
+ __JSON_Set_Key(key: __Virtual<string>, data: string, val_start: i32, val_end: i32, initializeDefaultValues: boolean): void {
198
+ ${this.currentClass.setDataStmts.join("\n ")}
203
199
  }
204
200
  `;
205
201
 
@@ -207,13 +203,13 @@ class AsJSONTransform extends BaseVisitor {
207
203
 
208
204
  if (this.currentClass.initializeStmts.length > 0) {
209
205
  initializeFunc = `
210
- @inline __JSON_Initialize(): void {
206
+ __JSON_Initialize(): void {
211
207
  ${this.currentClass.initializeStmts.join(";\n")};
212
208
  }
213
209
  `;
214
210
  } else {
215
211
  initializeFunc = `
216
- @inline __JSON_Initialize(): void {}
212
+ __JSON_Initialize(): void {}
217
213
  `;
218
214
  }
219
215
  const serializeMethod = SimpleParser.parseClassMember(serializeFunc, node);
@@ -226,13 +222,45 @@ class AsJSONTransform extends BaseVisitor {
226
222
  node.members.push(initializeMethod);
227
223
 
228
224
  this.schemasList.push(this.currentClass);
229
- //console.log(toString(node));
225
+ this.sources.add(node.name.range.source);
226
+
227
+ // Uncomment to see the generated code for debugging.
228
+ // console.log(serializeFunc);
229
+ // console.log(setKeyFunc);
230
+ // console.log(initializeFunc);
230
231
  }
232
+
231
233
  visitSource(node: Source): void {
232
234
  super.visitSource(node);
235
+
236
+ // Only add the import statement to sources that have JSON decorated classes.
237
+ if (!this.sources.has(node)) {
238
+ return;
239
+ }
240
+
241
+ // Note, the following one liner would be easier, but it fails with an assertion error
242
+ // because as-virtual's SimpleParser doesn't set the parser.currentSource correctly.
243
+ //
244
+ // const stmt = SimpleParser.parseTopLevelStatement('import { Virtual as __Virtual } from "as-virtual/assembly";');
245
+
246
+ // ... So we have to do it the long way:
247
+ const s = 'import { Virtual as __Virtual } from "as-virtual/assembly";'
248
+ const t = new Tokenizer(new Source(SourceKind.User, "index.ts", s));
249
+ const p = new Parser();
250
+ p.currentSource = t.source;
251
+ const stmt = p.parseTopLevelStatement(t)!;
252
+
253
+ // Add the import statement to the top of the source.
254
+ node.statements.unshift(stmt);
233
255
  }
234
256
  }
235
257
 
258
+ function encodeKey(aliasName: string): string {
259
+ return JSON.stringify(aliasName)
260
+ .replace(/\\/g, "\\\\")
261
+ .replace(/\`/g, '\\`');
262
+ }
263
+
236
264
  export default class Transformer extends Transform {
237
265
  // Trigger the transform after parse.
238
266
  afterParse(parser: Parser): void {