goscript 0.2.6 → 0.2.7

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.
Files changed (264) hide show
  1. package/cmd/goscript/cmd-compile.go +7 -0
  2. package/cmd/goscript/cmd_compile_test.go +83 -0
  3. package/compiler/compile-request.go +3 -0
  4. package/compiler/compiler-cache.go +828 -0
  5. package/compiler/compiler-cache_test.go +705 -0
  6. package/compiler/config.go +2 -0
  7. package/compiler/index.test.ts +26 -1
  8. package/compiler/index.ts +5 -0
  9. package/compiler/lowered-program.go +31 -20
  10. package/compiler/lowering.go +349 -93
  11. package/compiler/lowering_bench_test.go +1 -0
  12. package/compiler/override-facts.go +309 -8
  13. package/compiler/override-parity-verifier.go +45 -1
  14. package/compiler/override-parity-verifier_test.go +100 -0
  15. package/compiler/override-registry_test.go +1 -0
  16. package/compiler/package-graph.go +40 -12
  17. package/compiler/package-graph_test.go +29 -0
  18. package/compiler/runtime-contract.go +8 -0
  19. package/compiler/service.go +98 -11
  20. package/compiler/skeleton_test.go +110 -14
  21. package/compiler/typescript-emitter.go +120 -23
  22. package/dist/compiler/index.d.ts +2 -0
  23. package/dist/compiler/index.js +3 -0
  24. package/dist/compiler/index.js.map +1 -1
  25. package/dist/gs/builtin/builtin.d.ts +24 -33
  26. package/dist/gs/builtin/builtin.js +54 -61
  27. package/dist/gs/builtin/builtin.js.map +1 -1
  28. package/dist/gs/builtin/hostio.d.ts +1 -0
  29. package/dist/gs/builtin/hostio.js +1 -1
  30. package/dist/gs/builtin/hostio.js.map +1 -1
  31. package/dist/gs/builtin/index.d.ts +1 -0
  32. package/dist/gs/builtin/index.js +1 -0
  33. package/dist/gs/builtin/index.js.map +1 -1
  34. package/dist/gs/builtin/panic.d.ts +18 -0
  35. package/dist/gs/builtin/panic.js +98 -0
  36. package/dist/gs/builtin/panic.js.map +1 -0
  37. package/dist/gs/builtin/slice.d.ts +10 -0
  38. package/dist/gs/builtin/slice.js +110 -53
  39. package/dist/gs/builtin/slice.js.map +1 -1
  40. package/dist/gs/builtin/type.js +15 -3
  41. package/dist/gs/builtin/type.js.map +1 -1
  42. package/dist/gs/builtin/varRef.d.ts +1 -1
  43. package/dist/gs/builtin/varRef.js +3 -2
  44. package/dist/gs/builtin/varRef.js.map +1 -1
  45. package/dist/gs/bytes/bytes.gs.js +51 -38
  46. package/dist/gs/bytes/bytes.gs.js.map +1 -1
  47. package/dist/gs/bytes/reader.gs.d.ts +1 -1
  48. package/dist/gs/bytes/reader.gs.js +6 -7
  49. package/dist/gs/bytes/reader.gs.js.map +1 -1
  50. package/dist/gs/cmp/index.d.ts +1 -1
  51. package/dist/gs/cmp/index.js +43 -10
  52. package/dist/gs/cmp/index.js.map +1 -1
  53. package/dist/gs/context/context.d.ts +2 -2
  54. package/dist/gs/context/context.js +1 -1
  55. package/dist/gs/context/context.js.map +1 -1
  56. package/dist/gs/embed/index.js +1 -1
  57. package/dist/gs/embed/index.js.map +1 -1
  58. package/dist/gs/encoding/binary/index.js +201 -8
  59. package/dist/gs/encoding/binary/index.js.map +1 -1
  60. package/dist/gs/encoding/json/index.d.ts +5 -0
  61. package/dist/gs/encoding/json/index.js +388 -25
  62. package/dist/gs/encoding/json/index.js.map +1 -1
  63. package/dist/gs/errors/errors.js +17 -24
  64. package/dist/gs/errors/errors.js.map +1 -1
  65. package/dist/gs/fmt/fmt.js +129 -35
  66. package/dist/gs/fmt/fmt.js.map +1 -1
  67. package/dist/gs/golang.org/x/crypto/cryptobyte/index.js +1 -1
  68. package/dist/gs/golang.org/x/crypto/cryptobyte/index.js.map +1 -1
  69. package/dist/gs/internal/bytealg/index.js +43 -8
  70. package/dist/gs/internal/bytealg/index.js.map +1 -1
  71. package/dist/gs/internal/byteorder/index.d.ts +2 -2
  72. package/dist/gs/internal/byteorder/index.js +2 -2
  73. package/dist/gs/internal/byteorder/index.js.map +1 -1
  74. package/dist/gs/io/fs/format.js +2 -2
  75. package/dist/gs/io/fs/format.js.map +1 -1
  76. package/dist/gs/io/fs/fs.d.ts +1 -1
  77. package/dist/gs/io/fs/fs.js +1 -1
  78. package/dist/gs/io/fs/fs.js.map +1 -1
  79. package/dist/gs/io/io.d.ts +21 -21
  80. package/dist/gs/io/io.js +49 -50
  81. package/dist/gs/io/io.js.map +1 -1
  82. package/dist/gs/math/bits/index.js +26 -8
  83. package/dist/gs/math/bits/index.js.map +1 -1
  84. package/dist/gs/math/copysign.gs.js +10 -17
  85. package/dist/gs/math/copysign.gs.js.map +1 -1
  86. package/dist/gs/math/pow.gs.js +5 -0
  87. package/dist/gs/math/pow.gs.js.map +1 -1
  88. package/dist/gs/math/signbit.gs.js +6 -2
  89. package/dist/gs/math/signbit.gs.js.map +1 -1
  90. package/dist/gs/mime/index.js +1 -0
  91. package/dist/gs/mime/index.js.map +1 -1
  92. package/dist/gs/net/http/index.d.ts +6 -6
  93. package/dist/gs/net/http/index.js +507 -43
  94. package/dist/gs/net/http/index.js.map +1 -1
  95. package/dist/gs/os/stat.gs.d.ts +2 -2
  96. package/dist/gs/os/types.gs.d.ts +1 -1
  97. package/dist/gs/os/types.gs.js +1 -1
  98. package/dist/gs/os/types.gs.js.map +1 -1
  99. package/dist/gs/os/types_js.gs.d.ts +1 -1
  100. package/dist/gs/os/types_js.gs.js +7 -7
  101. package/dist/gs/os/types_js.gs.js.map +1 -1
  102. package/dist/gs/os/types_unix.gs.d.ts +1 -1
  103. package/dist/gs/os/types_unix.gs.js +1 -1
  104. package/dist/gs/os/types_unix.gs.js.map +1 -1
  105. package/dist/gs/os/zero_copy_posix.gs.d.ts +1 -1
  106. package/dist/gs/os/zero_copy_posix.gs.js +1 -1
  107. package/dist/gs/os/zero_copy_posix.gs.js.map +1 -1
  108. package/dist/gs/path/filepath/match.js +8 -4
  109. package/dist/gs/path/filepath/match.js.map +1 -1
  110. package/dist/gs/path/filepath/path.js +216 -42
  111. package/dist/gs/path/filepath/path.js.map +1 -1
  112. package/dist/gs/path/match.js +6 -3
  113. package/dist/gs/path/match.js.map +1 -1
  114. package/dist/gs/reflect/type.d.ts +5 -4
  115. package/dist/gs/reflect/type.js +29 -11
  116. package/dist/gs/reflect/type.js.map +1 -1
  117. package/dist/gs/slices/slices.js +11 -11
  118. package/dist/gs/slices/slices.js.map +1 -1
  119. package/dist/gs/strconv/atof.gs.js +156 -43
  120. package/dist/gs/strconv/atof.gs.js.map +1 -1
  121. package/dist/gs/strconv/atoi.gs.d.ts +3 -2
  122. package/dist/gs/strconv/atoi.gs.js +86 -67
  123. package/dist/gs/strconv/atoi.gs.js.map +1 -1
  124. package/dist/gs/strconv/ftoa.gs.js +73 -3
  125. package/dist/gs/strconv/ftoa.gs.js.map +1 -1
  126. package/dist/gs/strconv/itoa.gs.d.ts +4 -4
  127. package/dist/gs/strconv/itoa.gs.js +5 -4
  128. package/dist/gs/strconv/itoa.gs.js.map +1 -1
  129. package/dist/gs/strconv/quote.gs.d.ts +1 -1
  130. package/dist/gs/strconv/quote.gs.js +311 -103
  131. package/dist/gs/strconv/quote.gs.js.map +1 -1
  132. package/dist/gs/strings/reader.d.ts +1 -1
  133. package/dist/gs/strings/reader.js +8 -8
  134. package/dist/gs/strings/reader.js.map +1 -1
  135. package/dist/gs/strings/strings.js +87 -61
  136. package/dist/gs/strings/strings.js.map +1 -1
  137. package/dist/gs/sync/atomic/doc_64.gs.d.ts +14 -14
  138. package/dist/gs/sync/atomic/doc_64.gs.js +10 -10
  139. package/dist/gs/sync/atomic/doc_64.gs.js.map +1 -1
  140. package/dist/gs/sync/atomic/type.gs.d.ts +22 -22
  141. package/dist/gs/sync/atomic/type.gs.js +4 -4
  142. package/dist/gs/sync/atomic/type.gs.js.map +1 -1
  143. package/dist/gs/sync/sync.js +50 -12
  144. package/dist/gs/sync/sync.js.map +1 -1
  145. package/dist/gs/syscall/fs.d.ts +6 -6
  146. package/dist/gs/syscall/fs.js +1 -1
  147. package/dist/gs/syscall/fs.js.map +1 -1
  148. package/dist/gs/time/time.d.ts +18 -18
  149. package/dist/gs/time/time.js +58 -55
  150. package/dist/gs/time/time.js.map +1 -1
  151. package/dist/gs/unicode/tables.d.ts +11 -0
  152. package/dist/gs/unicode/tables.js +635 -0
  153. package/dist/gs/unicode/tables.js.map +1 -0
  154. package/dist/gs/unicode/unicode.d.ts +58 -38
  155. package/dist/gs/unicode/unicode.js +362 -278
  156. package/dist/gs/unicode/unicode.js.map +1 -1
  157. package/go.sum +13 -0
  158. package/gs/builtin/builtin.ts +83 -93
  159. package/gs/builtin/hostio.ts +1 -1
  160. package/gs/builtin/index.ts +1 -0
  161. package/gs/builtin/panic.test.ts +189 -0
  162. package/gs/builtin/panic.ts +107 -0
  163. package/gs/builtin/runtime-contract.test.ts +5 -5
  164. package/gs/builtin/slice.test.ts +23 -0
  165. package/gs/builtin/slice.ts +133 -95
  166. package/gs/builtin/type.ts +16 -3
  167. package/gs/builtin/varRef.ts +4 -2
  168. package/gs/builtin/wide-int.test.ts +41 -0
  169. package/gs/bytes/bytes.gs.ts +54 -41
  170. package/gs/bytes/bytes.test.ts +18 -1
  171. package/gs/bytes/reader.gs.ts +7 -8
  172. package/gs/cmp/index.test.ts +55 -0
  173. package/gs/cmp/index.ts +45 -9
  174. package/gs/context/context.ts +3 -3
  175. package/gs/embed/index.ts +2 -2
  176. package/gs/encoding/binary/index.test.ts +104 -0
  177. package/gs/encoding/binary/index.ts +259 -11
  178. package/gs/encoding/json/index.test.ts +107 -0
  179. package/gs/encoding/json/index.ts +400 -29
  180. package/gs/errors/errors.test.ts +44 -1
  181. package/gs/errors/errors.ts +15 -31
  182. package/gs/fmt/fmt.test.ts +70 -2
  183. package/gs/fmt/fmt.ts +128 -34
  184. package/gs/golang.org/x/crypto/cryptobyte/index.ts +1 -1
  185. package/gs/internal/bytealg/index.test.ts +26 -1
  186. package/gs/internal/bytealg/index.ts +44 -8
  187. package/gs/internal/byteorder/index.ts +6 -4
  188. package/gs/io/fs/format.ts +2 -2
  189. package/gs/io/fs/fs.ts +2 -2
  190. package/gs/io/fs/stat.test.ts +2 -2
  191. package/gs/io/fs/sub.test.ts +2 -2
  192. package/gs/io/fs/walk.test.ts +2 -2
  193. package/gs/io/io.test.ts +47 -5
  194. package/gs/io/io.ts +73 -73
  195. package/gs/io/limit.test.ts +103 -0
  196. package/gs/math/bits/index.test.ts +128 -0
  197. package/gs/math/bits/index.ts +26 -8
  198. package/gs/math/copysign.gs.test.ts +3 -1
  199. package/gs/math/copysign.gs.ts +10 -22
  200. package/gs/math/pow.gs.test.ts +4 -5
  201. package/gs/math/pow.gs.ts +5 -0
  202. package/gs/math/signbit.gs.test.ts +2 -1
  203. package/gs/math/signbit.gs.ts +6 -3
  204. package/gs/mime/index.ts +1 -0
  205. package/gs/net/http/index.test.ts +683 -2
  206. package/gs/net/http/index.ts +598 -57
  207. package/gs/net/http/meta.json +3 -0
  208. package/gs/os/stat.gs.ts +2 -2
  209. package/gs/os/types.gs.ts +2 -2
  210. package/gs/os/types_js.gs.ts +9 -9
  211. package/gs/os/types_unix.gs.ts +2 -2
  212. package/gs/os/zero_copy_posix.gs.ts +2 -2
  213. package/gs/path/filepath/match.test.ts +16 -0
  214. package/gs/path/filepath/match.ts +8 -4
  215. package/gs/path/filepath/path.test.ts +91 -9
  216. package/gs/path/filepath/path.ts +223 -49
  217. package/gs/path/match.test.ts +32 -0
  218. package/gs/path/match.ts +6 -3
  219. package/gs/reflect/deepequal.test.ts +1 -1
  220. package/gs/reflect/field.test.ts +1 -1
  221. package/gs/reflect/function-types.test.ts +6 -6
  222. package/gs/reflect/sliceat.test.ts +13 -13
  223. package/gs/reflect/structof.test.ts +4 -4
  224. package/gs/reflect/type.ts +34 -14
  225. package/gs/reflect/typefor.test.ts +5 -5
  226. package/gs/runtime/pprof/index.test.ts +20 -0
  227. package/gs/runtime/trace/index.test.ts +3 -0
  228. package/gs/slices/slices.test.ts +31 -0
  229. package/gs/slices/slices.ts +11 -11
  230. package/gs/strconv/append.test.ts +99 -0
  231. package/gs/strconv/atof.gs.ts +156 -42
  232. package/gs/strconv/atof.test.ts +45 -0
  233. package/gs/strconv/atoi.gs.ts +87 -69
  234. package/gs/strconv/atoi.test.ts +49 -0
  235. package/gs/strconv/ftoa.gs.ts +85 -10
  236. package/gs/strconv/ftoa.test.ts +43 -0
  237. package/gs/strconv/itoa.gs.ts +10 -9
  238. package/gs/strconv/quote.gs.ts +335 -108
  239. package/gs/strconv/quote.test.ts +111 -0
  240. package/gs/strings/reader.test.ts +10 -10
  241. package/gs/strings/reader.ts +9 -9
  242. package/gs/strings/strings.test.ts +18 -5
  243. package/gs/strings/strings.ts +81 -68
  244. package/gs/sync/atomic/doc_64.gs.ts +24 -24
  245. package/gs/sync/atomic/doc_64.test.ts +5 -5
  246. package/gs/sync/atomic/type.gs.ts +28 -28
  247. package/gs/sync/sync.test.ts +109 -1
  248. package/gs/sync/sync.ts +46 -12
  249. package/gs/syscall/fs.ts +8 -8
  250. package/gs/syscall/net.test.ts +1 -1
  251. package/gs/time/parse.test.ts +45 -0
  252. package/gs/time/time.test.ts +46 -23
  253. package/gs/time/time.ts +69 -66
  254. package/gs/unicode/gen.go +198 -0
  255. package/gs/unicode/tables.ts +646 -0
  256. package/gs/unicode/unicode.test.ts +69 -0
  257. package/gs/unicode/unicode.ts +396 -312
  258. package/package.json +1 -1
  259. package/dist/gs/github.com/aperturerobotics/util/conc/index.d.ts +0 -20
  260. package/dist/gs/github.com/aperturerobotics/util/conc/index.js +0 -134
  261. package/dist/gs/github.com/aperturerobotics/util/conc/index.js.map +0 -1
  262. package/gs/github.com/aperturerobotics/util/conc/index.test.ts +0 -30
  263. package/gs/github.com/aperturerobotics/util/conc/index.ts +0 -172
  264. package/gs/github.com/aperturerobotics/util/conc/meta.json +0 -9
@@ -1,165 +1,393 @@
1
1
  import * as $ from "@goscript/builtin/index.js";
2
+ import * as unicode from "@goscript/unicode/index.js";
2
3
  import { ErrSyntax } from "./atoi.gs.js";
3
4
 
5
+ const lowerhex = "0123456789abcdef";
6
+
7
+ // hexEscape emits Go's \xNN, \uNNNN, or \UNNNNNNNN escape with width hex digits.
8
+ function hexEscape(prefix: string, value: number, width: number): string {
9
+ let out = prefix;
10
+ for (let shift = (width - 1) * 4; shift >= 0; shift -= 4) {
11
+ out += lowerhex[(value >> shift) & 0xf];
12
+ }
13
+ return out;
14
+ }
15
+
16
+ // appendEscapedRune returns the Go-escaped form of one rune, matching
17
+ // strconv.appendEscapedRune: printable runes pass through, the standard control
18
+ // escapes (\a \b \f \n \r \t \v) are emitted by name, control and DEL bytes
19
+ // become \xNN, invalid runes fold to U+FFFD, and the rest become \u/\U. This
20
+ // replaces the prior JSON.stringify / fromCharCode shortcuts, which used the
21
+ // wrong escape set and truncated runes above U+FFFF.
22
+ function appendEscapedRune(
23
+ r: number,
24
+ quote: number,
25
+ asciiOnly: boolean,
26
+ graphicOnly: boolean,
27
+ ): string {
28
+ if (r === quote || r === 0x5c /* backslash */) {
29
+ return "\\" + String.fromCharCode(r);
30
+ }
31
+ if (asciiOnly) {
32
+ if (r < 0x80 && unicode.IsPrint(r)) {
33
+ return String.fromCharCode(r);
34
+ }
35
+ } else if (unicode.IsPrint(r) || (graphicOnly && unicode.IsGraphic(r))) {
36
+ return $.runeToString(r);
37
+ }
38
+ switch (r) {
39
+ case 0x07:
40
+ return "\\a";
41
+ case 0x08:
42
+ return "\\b";
43
+ case 0x0c:
44
+ return "\\f";
45
+ case 0x0a:
46
+ return "\\n";
47
+ case 0x0d:
48
+ return "\\r";
49
+ case 0x09:
50
+ return "\\t";
51
+ case 0x0b:
52
+ return "\\v";
53
+ }
54
+ if (r < 0x20 || r === 0x7f) {
55
+ return hexEscape("\\x", r & 0xff, 2);
56
+ }
57
+ if (r < 0 || r > 0x10ffff || (r >= 0xd800 && r <= 0xdfff)) {
58
+ r = 0xfffd; // !utf8.ValidRune: fold to RuneError before emitting \u.
59
+ }
60
+ if (r < 0x10000) {
61
+ return hexEscape("\\u", r, 4);
62
+ }
63
+ return hexEscape("\\U", r, 8);
64
+ }
65
+
66
+ // quoteWith renders s as a Go quoted literal using quote as the delimiter.
67
+ function quoteWith(
68
+ s: string,
69
+ quote: number,
70
+ asciiOnly: boolean,
71
+ graphicOnly: boolean,
72
+ ): string {
73
+ let out = String.fromCharCode(quote);
74
+ for (const ch of s) {
75
+ out += appendEscapedRune(ch.codePointAt(0)!, quote, asciiOnly, graphicOnly);
76
+ }
77
+ return out + String.fromCharCode(quote);
78
+ }
79
+
80
+ // quoteRuneWith renders a single rune as a Go quoted character literal.
81
+ function quoteRuneWith(
82
+ r: number,
83
+ quote: number,
84
+ asciiOnly: boolean,
85
+ graphicOnly: boolean,
86
+ ): string {
87
+ return (
88
+ String.fromCharCode(quote) +
89
+ appendEscapedRune(r, quote, asciiOnly, graphicOnly) +
90
+ String.fromCharCode(quote)
91
+ );
92
+ }
93
+
4
94
  // Quote returns a double-quoted Go string literal representing s.
5
95
  // The returned string uses Go escape sequences (\t, \n, \xFF, \u0100) for control characters and non-printable characters.
6
96
  export function Quote(s: string): string {
7
- return JSON.stringify(s);
97
+ return quoteWith(s, 0x22 /* " */, false, false);
8
98
  }
9
99
 
10
100
  // QuoteToASCII returns a double-quoted Go string literal representing s.
11
101
  // The returned string uses Go escape sequences (\t, \n, \xFF, \u0100) for control characters and non-ASCII characters.
12
102
  export function QuoteToASCII(s: string): string {
13
- // For simplicity, use JSON.stringify and then escape non-ASCII
14
- const quoted = JSON.stringify(s);
15
- return quoted.replace(/[\u0080-\uFFFF]/g, (match) => {
16
- const code = match.charCodeAt(0);
17
- if (code <= 0xFF) {
18
- return '\\x' + code.toString(16).padStart(2, '0');
19
- } else {
20
- return '\\u' + code.toString(16).padStart(4, '0');
21
- }
22
- });
103
+ return quoteWith(s, 0x22, true, false);
23
104
  }
24
105
 
25
106
  // QuoteToGraphic returns a double-quoted Go string literal representing s.
26
107
  // The returned string leaves Unicode graphic characters unchanged.
27
108
  export function QuoteToGraphic(s: string): string {
28
- return Quote(s); // Simplified
109
+ return quoteWith(s, 0x22, false, true);
29
110
  }
30
111
 
31
112
  // QuoteRune returns a single-quoted Go character literal representing the rune.
32
113
  export function QuoteRune(r: number): string {
33
- const char = String.fromCharCode(r);
34
- if (r === 39) { // single quote
35
- return "'\\'";
36
- }
37
- if (r === 92) { // backslash
38
- return "'\\\\'";
39
- }
40
- if (r >= 32 && r <= 126) { // printable ASCII
41
- return "'" + char + "'";
42
- }
43
- // Use escape sequences for non-printable
44
- if (r <= 0xFF) {
45
- return "'\\x" + r.toString(16).padStart(2, '0') + "'";
46
- }
47
- return "'\\u" + r.toString(16).padStart(4, '0') + "'";
114
+ return quoteRuneWith(r, 0x27 /* ' */, false, false);
48
115
  }
49
116
 
50
117
  // QuoteRuneToASCII returns a single-quoted Go character literal representing the rune.
51
118
  export function QuoteRuneToASCII(r: number): string {
52
- return QuoteRune(r); // Same as QuoteRune for simplicity
119
+ return quoteRuneWith(r, 0x27, true, false);
53
120
  }
54
121
 
55
122
  // QuoteRuneToGraphic returns a single-quoted Go character literal representing the rune.
56
123
  export function QuoteRuneToGraphic(r: number): string {
57
- return QuoteRune(r); // Same as QuoteRune for simplicity
124
+ return quoteRuneWith(r, 0x27, false, true);
58
125
  }
59
126
 
60
- // CanBackquote reports whether the string s can be represented unchanged as a single-line backquoted string.
127
+ // CanBackquote reports whether the string s can be represented unchanged as a
128
+ // single-line backquoted string. Go decodes UTF-8 runes: a tab is allowed, but
129
+ // other control bytes below space, a backtick, DEL, an invalid byte
130
+ // (utf8.RuneError at width 1), and a BOM (U+FEFF, invisible) are rejected.
61
131
  export function CanBackquote(s: string): boolean {
62
- // Check if string contains backticks or control characters
63
- for (let i = 0; i < s.length; i++) {
64
- const c = s.charCodeAt(i);
65
- if (c === 96 || c < 32 || c === 127) { // backtick or control character
132
+ const bytes = $.stringToBytes(s);
133
+ let i = 0;
134
+ while (i < bytes.length) {
135
+ const [r, wid] = decodeRune(bytes, i);
136
+ i += wid;
137
+ if (wid > 1) {
138
+ if (r === 0xfeff) {
139
+ return false; // BOMs are invisible and should not be quoted.
140
+ }
141
+ continue; // Other multibyte runes are valid and assumed printable.
142
+ }
143
+ if (r === 0xfffd) {
144
+ return false; // utf8.RuneError from an invalid single byte.
145
+ }
146
+ if ((r < 0x20 && r !== 0x09) || r === 0x60 || r === 0x7f) {
66
147
  return false;
67
148
  }
68
149
  }
69
150
  return true;
70
151
  }
71
152
 
72
- // Unquote interprets s as a single-quoted, double-quoted, or backquoted Go string literal.
73
- export function Unquote(s: string): [string, $.GoError] {
74
- const n = s.length;
75
- if (n < 2) {
76
- return ["", ErrSyntax];
153
+ // unhex returns the value of the hex digit byte b and whether b is a hex digit.
154
+ function unhex(b: number): [number, boolean] {
155
+ if (b >= 0x30 && b <= 0x39) {
156
+ return [b - 0x30, true];
77
157
  }
78
-
79
- const quote = s[0];
80
- if (quote !== s[n-1]) {
81
- return ["", ErrSyntax];
158
+ if (b >= 0x61 && b <= 0x66) {
159
+ return [b - 0x61 + 10, true];
160
+ }
161
+ if (b >= 0x41 && b <= 0x46) {
162
+ return [b - 0x41 + 10, true];
82
163
  }
164
+ return [0, false];
165
+ }
83
166
 
84
- s = s.slice(1, n-1);
167
+ // decodeRune decodes the UTF-8 rune at bytes[pos], returning the rune and the
168
+ // number of bytes consumed. Invalid sequences decode to U+FFFD with width 1,
169
+ // matching utf8.DecodeRune.
170
+ function decodeRune(bytes: Uint8Array, pos: number): [number, number] {
171
+ const b0 = bytes[pos];
172
+ if (b0 < 0x80) {
173
+ return [b0, 1];
174
+ }
175
+ let size: number;
176
+ let r: number;
177
+ if ((b0 & 0xe0) === 0xc0) {
178
+ size = 2;
179
+ r = b0 & 0x1f;
180
+ } else if ((b0 & 0xf0) === 0xe0) {
181
+ size = 3;
182
+ r = b0 & 0x0f;
183
+ } else if ((b0 & 0xf8) === 0xf0) {
184
+ size = 4;
185
+ r = b0 & 0x07;
186
+ } else {
187
+ return [0xfffd, 1];
188
+ }
189
+ if (pos + size > bytes.length) {
190
+ return [0xfffd, 1];
191
+ }
192
+ for (let j = 1; j < size; j++) {
193
+ const cb = bytes[pos + j];
194
+ if ((cb & 0xc0) !== 0x80) {
195
+ return [0xfffd, 1];
196
+ }
197
+ r = (r << 6) | (cb & 0x3f);
198
+ }
199
+ return [r, size];
200
+ }
85
201
 
86
- if (quote === '`') {
87
- // Backquoted string - no escapes processed
88
- if (s.includes('`')) {
89
- return ["", ErrSyntax];
202
+ // unquoteCharBytes decodes the first character or byte in the escaped byte
203
+ // sequence starting at pos, returning [value, multibyte, newPos, err]. It is the
204
+ // byte-oriented core shared by the exported UnquoteChar and Unquote so that
205
+ // \xNN raw bytes and multibyte source runes round-trip exactly as in Go.
206
+ function unquoteCharBytes(
207
+ bytes: Uint8Array,
208
+ pos: number,
209
+ quote: number,
210
+ ): [number, boolean, number, $.GoError] {
211
+ if (pos >= bytes.length) {
212
+ return [0, false, pos, ErrSyntax];
213
+ }
214
+ const c = bytes[pos];
215
+ if (c === quote && (quote === 0x27 || quote === 0x22)) {
216
+ return [0, false, pos, ErrSyntax];
217
+ }
218
+ if (c >= 0x80) {
219
+ const [r, size] = decodeRune(bytes, pos);
220
+ return [r, true, pos + size, null];
221
+ }
222
+ if (c !== 0x5c /* backslash */) {
223
+ return [c, false, pos + 1, null];
224
+ }
225
+ if (pos + 1 >= bytes.length) {
226
+ return [0, false, pos, ErrSyntax];
227
+ }
228
+ const e = bytes[pos + 1];
229
+ let p = pos + 2;
230
+ switch (e) {
231
+ case 0x61: // \a
232
+ return [0x07, false, p, null];
233
+ case 0x62: // \b
234
+ return [0x08, false, p, null];
235
+ case 0x66: // \f
236
+ return [0x0c, false, p, null];
237
+ case 0x6e: // \n
238
+ return [0x0a, false, p, null];
239
+ case 0x72: // \r
240
+ return [0x0d, false, p, null];
241
+ case 0x74: // \t
242
+ return [0x09, false, p, null];
243
+ case 0x76: // \v
244
+ return [0x0b, false, p, null];
245
+ case 0x78: // \x
246
+ case 0x75: // \u
247
+ case 0x55: { // \U
248
+ const n = e === 0x78 ? 2 : e === 0x75 ? 4 : 8;
249
+ if (p + n > bytes.length) {
250
+ return [0, false, pos, ErrSyntax];
251
+ }
252
+ let v = 0;
253
+ for (let j = 0; j < n; j++) {
254
+ const [x, ok] = unhex(bytes[p + j]);
255
+ if (!ok) {
256
+ return [0, false, pos, ErrSyntax];
257
+ }
258
+ v = (v << 4) | x;
259
+ }
260
+ p += n;
261
+ if (e === 0x78) {
262
+ return [v, false, p, null]; // single byte
263
+ }
264
+ if (v < 0 || v > 0x10ffff || (v >= 0xd800 && v <= 0xdfff)) {
265
+ return [0, false, pos, ErrSyntax]; // !utf8.ValidRune
266
+ }
267
+ return [v, true, p, null];
90
268
  }
91
- if (s.includes('\r')) {
92
- return ["", ErrSyntax];
269
+ case 0x30:
270
+ case 0x31:
271
+ case 0x32:
272
+ case 0x33:
273
+ case 0x34:
274
+ case 0x35:
275
+ case 0x36:
276
+ case 0x37: { // octal \N
277
+ let v = e - 0x30;
278
+ if (p + 2 > bytes.length) {
279
+ return [0, false, pos, ErrSyntax];
280
+ }
281
+ for (let j = 0; j < 2; j++) {
282
+ const x = bytes[p + j] - 0x30;
283
+ if (x < 0 || x > 7) {
284
+ return [0, false, pos, ErrSyntax];
285
+ }
286
+ v = (v << 3) | x;
287
+ }
288
+ p += 2;
289
+ if (v > 255) {
290
+ return [0, false, pos, ErrSyntax];
291
+ }
292
+ return [v, false, p, null];
93
293
  }
94
- return [s, null];
294
+ case 0x5c: // backslash
295
+ return [0x5c, false, p, null];
296
+ case 0x27: // '
297
+ case 0x22: // "
298
+ if (e !== quote) {
299
+ return [0, false, pos, ErrSyntax];
300
+ }
301
+ return [e, false, p, null];
302
+ default:
303
+ return [0, false, pos, ErrSyntax];
95
304
  }
305
+ }
96
306
 
97
- if (quote !== '"' && quote !== "'") {
98
- return ["", ErrSyntax];
307
+ // UnquoteChar decodes the first character or byte in the escaped string or
308
+ // character literal represented by the string s. quote is the delimiting byte
309
+ // (' or "), or 0 to disallow neither.
310
+ export function UnquoteChar(s: string, quote: number): [number, boolean, string, $.GoError] {
311
+ const bytes = $.stringToBytes(s);
312
+ const [value, multibyte, newPos, err] = unquoteCharBytes(bytes, 0, quote);
313
+ if (err !== null) {
314
+ return [0, false, "", err];
99
315
  }
316
+ return [value, multibyte, $.bytesToString(bytes.subarray(newPos)), null];
317
+ }
100
318
 
101
- // Use JSON.parse for double-quoted strings as a starting point
102
- if (quote === '"') {
103
- try {
104
- const result = JSON.parse('"' + s + '"');
105
- return [result, null];
106
- } catch {
107
- return ["", ErrSyntax];
108
- }
319
+ // Unquote interprets s as a single-quoted, double-quoted, or backquoted Go
320
+ // string literal, returning the string value that s quotes.
321
+ export function Unquote(s: string): [string, $.GoError] {
322
+ const bytes = $.stringToBytes(s);
323
+ const n = bytes.length;
324
+ if (n < 2) {
325
+ return ["", ErrSyntax];
109
326
  }
327
+ const quote = bytes[0];
328
+ if (quote !== bytes[n - 1]) {
329
+ return ["", ErrSyntax];
330
+ }
331
+ const inner = bytes.subarray(1, n - 1);
110
332
 
111
- // Single-quoted string - should contain single rune
112
- if (quote === "'") {
113
- // Simplified: just handle basic cases
114
- if (s.length === 1) {
115
- return [s, null];
333
+ if (quote === 0x60 /* ` */) {
334
+ for (let i = 0; i < inner.length; i++) {
335
+ if (inner[i] === 0x60) {
336
+ return ["", ErrSyntax];
337
+ }
116
338
  }
117
- if (s.length === 2 && s[0] === '\\') {
118
- switch (s[1]) {
119
- case 'n': return ['\n', null];
120
- case 't': return ['\t', null];
121
- case 'r': return ['\r', null];
122
- case '\\': return ['\\', null];
123
- case "'": return ["'", null];
124
- default: return ["", ErrSyntax];
339
+ if (inner.includes(0x0d)) {
340
+ const out: number[] = [];
341
+ for (let i = 0; i < inner.length; i++) {
342
+ if (inner[i] !== 0x0d) {
343
+ out.push(inner[i]);
344
+ }
125
345
  }
346
+ return [$.bytesToString(Uint8Array.from(out)), null];
126
347
  }
127
- return ["", ErrSyntax];
348
+ return [$.bytesToString(inner), null];
128
349
  }
129
350
 
130
- return ["", ErrSyntax];
131
- }
132
-
133
- // UnquoteChar decodes the first character or byte in the escaped string or character literal represented by the string s.
134
- export function UnquoteChar(s: string, quote: number): [number, boolean, string, $.GoError] {
135
- // Simplified implementation
136
- if (s.length === 0) {
137
- return [0, false, "", ErrSyntax];
138
- }
139
-
140
- const c = s.charCodeAt(0);
141
- if (c === quote) {
142
- return [0, false, "", ErrSyntax];
351
+ if (quote !== 0x22 && quote !== 0x27) {
352
+ return ["", ErrSyntax];
143
353
  }
144
-
145
- if (c !== 92) { // not backslash
146
- return [c, c >= 128, s.slice(1), null];
354
+ if (inner.includes(0x0a)) {
355
+ return ["", ErrSyntax]; // newline not allowed in " or ' literal
147
356
  }
148
-
149
- // Handle escape sequence - simplified
150
- if (s.length < 2) {
151
- return [0, false, "", ErrSyntax];
357
+
358
+ // Fast path: no escape and no embedded quote byte.
359
+ if (!inner.includes(0x5c) && !inner.includes(quote)) {
360
+ if (quote === 0x22) {
361
+ return [$.bytesToString(inner), null];
362
+ }
363
+ // single-quoted: must hold exactly one rune
364
+ const [r, size] = decodeRune(inner, 0);
365
+ if (size === inner.length && (r !== 0xfffd || size !== 1)) {
366
+ return [$.bytesToString(inner), null];
367
+ }
152
368
  }
153
-
154
- switch (s[1]) {
155
- case 'n': return [10, false, s.slice(2), null]; // \n
156
- case 't': return [9, false, s.slice(2), null]; // \t
157
- case 'r': return [13, false, s.slice(2), null]; // \r
158
- case '\\': return [92, false, s.slice(2), null]; // \\
159
- case '"': return [34, false, s.slice(2), null]; // \"
160
- case "'": return [39, false, s.slice(2), null]; // \'
161
- default: return [0, false, "", ErrSyntax];
369
+
370
+ const out: number[] = [];
371
+ let pos = 0;
372
+ while (pos < inner.length) {
373
+ const [c, multibyte, newPos, err] = unquoteCharBytes(inner, pos, quote);
374
+ if (err !== null) {
375
+ return ["", err];
376
+ }
377
+ pos = newPos;
378
+ if (c < 0x80 || !multibyte) {
379
+ out.push(c & 0xff);
380
+ } else {
381
+ const enc = $.stringToBytes($.runeToString(c));
382
+ for (let j = 0; j < enc.length; j++) {
383
+ out.push(enc[j]);
384
+ }
385
+ }
386
+ if (quote === 0x27 && pos < inner.length) {
387
+ return ["", ErrSyntax]; // single-quoted must be one character
388
+ }
162
389
  }
390
+ return [$.bytesToString(Uint8Array.from(out)), null];
163
391
  }
164
392
 
165
393
  // QuotedPrefix returns the quoted string (as understood by Unquote) at the prefix of s.
@@ -235,11 +463,10 @@ export function AppendQuoteRuneToGraphic(dst: $.Bytes, r: number): $.Bytes {
235
463
 
236
464
  // IsPrint reports whether the rune is defined as printable by Go.
237
465
  export function IsPrint(r: number): boolean {
238
- // Simplified: consider ASCII printable characters
239
- return r >= 32 && r <= 126;
466
+ return unicode.IsPrint(r);
240
467
  }
241
468
 
242
469
  // IsGraphic reports whether the rune is defined as a Graphic by Unicode.
243
470
  export function IsGraphic(r: number): boolean {
244
- return IsPrint(r); // Simplified
245
- }
471
+ return unicode.IsGraphic(r);
472
+ }
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ CanBackquote,
5
+ Quote,
6
+ QuoteRune,
7
+ QuoteToASCII,
8
+ Unquote,
9
+ UnquoteChar,
10
+ } from './quote.gs.js'
11
+
12
+ // Expected outputs are the literal strings Go's strconv produces (go1.26.4).
13
+ describe('strconv.Quote (Go escape semantics)', () => {
14
+ it('passes printable ASCII and Unicode through unchanged', () => {
15
+ expect(Quote('hello')).toBe('"hello"')
16
+ // CJK is printable; Go leaves graphic Unicode in place.
17
+ expect(Quote('背景')).toBe('"背景"')
18
+ // U+1F600 is graphic; not truncated and not escaped.
19
+ expect(Quote('😀')).toBe('"😀"')
20
+ })
21
+
22
+ it('escapes the quote and backslash', () => {
23
+ expect(Quote('a"b\\c')).toBe('"a\\"b\\\\c"')
24
+ })
25
+
26
+ it('uses Go named escapes including \\a and \\v that JSON lacks', () => {
27
+ expect(Quote('a\tb\n')).toBe('"a\\tb\\n"')
28
+ expect(Quote('\x07\x0b')).toBe('"\\a\\v"')
29
+ })
30
+
31
+ it('uses \\xNN for control bytes and DEL, not \\uNNNN', () => {
32
+ expect(Quote('\x00\x1f')).toBe('"\\x00\\x1f"')
33
+ expect(Quote('\x7f')).toBe('"\\x7f"')
34
+ })
35
+ })
36
+
37
+ describe('strconv.QuoteToASCII', () => {
38
+ it('escapes non-ASCII as \\u and astral as \\U', () => {
39
+ expect(QuoteToASCII('背景')).toBe('"\\u80cc\\u666f"')
40
+ expect(QuoteToASCII('😀')).toBe('"\\U0001f600"')
41
+ expect(QuoteToASCII('hi')).toBe('"hi"')
42
+ })
43
+ })
44
+
45
+ describe('strconv.QuoteRune', () => {
46
+ it('quotes printable, control, and astral runes like Go', () => {
47
+ expect(QuoteRune('a'.charCodeAt(0))).toBe("'a'")
48
+ expect(QuoteRune(0x09)).toBe("'\\t'")
49
+ expect(QuoteRune(0x1f600)).toBe("'😀'")
50
+ expect(QuoteRune(0x27)).toBe("'\\''")
51
+ expect(QuoteRune(0x00)).toBe("'\\x00'")
52
+ })
53
+
54
+ it('folds an invalid rune to the U+FFFD escape', () => {
55
+ expect(QuoteRune(0x110000)).toBe("'\\ufffd'")
56
+ })
57
+ })
58
+
59
+ // Ground truth captured from go1.26.4 strconv.Unquote / CanBackquote /
60
+ // UnquoteChar over the same inputs.
61
+ describe('strconv.Unquote (Go escape decoding)', () => {
62
+ it('decodes \\x, octal, \\u, and \\U escapes', () => {
63
+ expect(Unquote('"\\x41"')).toEqual(['A', null])
64
+ expect(Unquote('"\\101"')).toEqual(['A', null]) // octal 101 = 'A'
65
+ expect(Unquote('"\\u0041"')).toEqual(['A', null])
66
+ expect(Unquote('"\\U0001F600"')).toEqual(['😀', null])
67
+ })
68
+
69
+ it('decodes named escapes and passes printable Unicode through', () => {
70
+ expect(Unquote('"a\\tb"')).toEqual(['a\tb', null])
71
+ expect(Unquote('"é"')).toEqual(['é', null])
72
+ })
73
+
74
+ it('handles backquoted strings without processing escapes', () => {
75
+ expect(Unquote('`raw\\tno-escape`')).toEqual(['raw\\tno-escape', null])
76
+ expect(Unquote('`with`back`')[1]).not.toBeNull() // embedded backtick fails
77
+ })
78
+
79
+ it('requires single-quoted literals to hold exactly one rune', () => {
80
+ expect(Unquote("'a'")).toEqual(['a', null])
81
+ expect(Unquote("'\\x41'")).toEqual(['A', null])
82
+ expect(Unquote("'ab'")[1]).not.toBeNull()
83
+ })
84
+
85
+ it('rejects malformed input', () => {
86
+ expect(Unquote('"')[1]).not.toBeNull()
87
+ expect(Unquote('"\\x4"')[1]).not.toBeNull() // short hex
88
+ expect(Unquote('"a')[1]).not.toBeNull() // unterminated
89
+ })
90
+ })
91
+
92
+ describe('strconv.CanBackquote (Go control rules)', () => {
93
+ it('allows tab but rejects other control bytes, backtick, DEL, and BOM', () => {
94
+ expect(CanBackquote('a\tb')).toBe(true)
95
+ expect(CanBackquote('plain text')).toBe(true)
96
+ expect(CanBackquote('a\nb')).toBe(false)
97
+ expect(CanBackquote('a`b')).toBe(false)
98
+ expect(CanBackquote('a\x7fb')).toBe(false)
99
+ expect(CanBackquote('\uFEFF')).toBe(false)
100
+ })
101
+ })
102
+
103
+ describe('strconv.UnquoteChar (tail + multibyte)', () => {
104
+ it('decodes one escape and returns the remaining tail', () => {
105
+ expect(UnquoteChar('\\x41rest', 0x22)).toEqual([0x41, false, 'rest', null])
106
+ })
107
+
108
+ it('flags multibyte runes from \\u escapes', () => {
109
+ expect(UnquoteChar('\\u00e9z', 0x22)).toEqual([0xe9, true, 'z', null])
110
+ })
111
+ })
@@ -126,8 +126,8 @@ describe('strings/Reader', () => {
126
126
  it('should seek from start', () => {
127
127
  const r = new Reader({ s: 'hello world' })
128
128
 
129
- const [pos, err] = r.Seek(6, io.SeekStart)
130
- expect(pos).toBe(6)
129
+ const [pos, err] = r.Seek(6n, io.SeekStart)
130
+ expect(pos).toBe(6n)
131
131
  expect(err).toBeNull()
132
132
  expect(r.Len()).toBe(5)
133
133
  })
@@ -136,32 +136,32 @@ describe('strings/Reader', () => {
136
136
  const r = new Reader({ s: 'hello world' })
137
137
  r.ReadByte() // advance to position 1
138
138
 
139
- const [pos, err] = r.Seek(2, io.SeekCurrent)
140
- expect(pos).toBe(3)
139
+ const [pos, err] = r.Seek(2n, io.SeekCurrent)
140
+ expect(pos).toBe(3n)
141
141
  expect(err).toBeNull()
142
142
  })
143
143
 
144
144
  it('should seek from end', () => {
145
145
  const r = new Reader({ s: 'hello world' })
146
146
 
147
- const [pos, err] = r.Seek(-5, io.SeekEnd)
148
- expect(pos).toBe(6)
147
+ const [pos, err] = r.Seek(-5n, io.SeekEnd)
148
+ expect(pos).toBe(6n)
149
149
  expect(err).toBeNull()
150
150
  })
151
151
 
152
152
  it('should handle invalid whence in seek', () => {
153
153
  const r = new Reader({ s: 'hello' })
154
154
 
155
- const [pos, err] = r.Seek(0, 999)
156
- expect(pos).toBe(0)
155
+ const [pos, err] = r.Seek(0n, 999)
156
+ expect(pos).toBe(0n)
157
157
  expect(err).not.toBeNull()
158
158
  })
159
159
 
160
160
  it('should handle negative position in seek', () => {
161
161
  const r = new Reader({ s: 'hello' })
162
162
 
163
- const [pos, err] = r.Seek(-10, io.SeekStart)
164
- expect(pos).toBe(0)
163
+ const [pos, err] = r.Seek(-10n, io.SeekStart)
164
+ expect(pos).toBe(0n)
165
165
  expect(err).not.toBeNull()
166
166
  })
167
167