goscript 0.1.2 → 0.1.4

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 (138) hide show
  1. package/cmd/goscript/cmd_compile.go +28 -8
  2. package/cmd/goscript/cmd_compile_test.go +105 -6
  3. package/compiler/build-flags.go +9 -10
  4. package/compiler/gotest/runner_test.go +127 -0
  5. package/compiler/lowered-program.go +1 -0
  6. package/compiler/lowering.go +1325 -194
  7. package/compiler/lowering_bench_test.go +350 -0
  8. package/compiler/override-registry_test.go +43 -0
  9. package/compiler/package-graph.go +61 -4
  10. package/compiler/package-graph_test.go +30 -0
  11. package/compiler/semantic-model-types.go +8 -0
  12. package/compiler/semantic-model.go +447 -22
  13. package/compiler/semantic-model_test.go +138 -0
  14. package/compiler/skeleton_test.go +1436 -50
  15. package/compiler/typescript-emitter.go +47 -4
  16. package/dist/gs/builtin/builtin.d.ts +2 -2
  17. package/dist/gs/builtin/builtin.js +20 -0
  18. package/dist/gs/builtin/builtin.js.map +1 -1
  19. package/dist/gs/builtin/channel.js +36 -9
  20. package/dist/gs/builtin/channel.js.map +1 -1
  21. package/dist/gs/builtin/slice.js +5 -0
  22. package/dist/gs/builtin/slice.js.map +1 -1
  23. package/dist/gs/builtin/type.d.ts +1 -1
  24. package/dist/gs/builtin/type.js +80 -8
  25. package/dist/gs/builtin/type.js.map +1 -1
  26. package/dist/gs/bytes/bytes.gs.d.ts +7 -5
  27. package/dist/gs/bytes/bytes.gs.js +10 -4
  28. package/dist/gs/bytes/bytes.gs.js.map +1 -1
  29. package/dist/gs/compress/zlib/index.d.ts +3 -3
  30. package/dist/gs/compress/zlib/index.js +88 -26
  31. package/dist/gs/compress/zlib/index.js.map +1 -1
  32. package/dist/gs/crypto/sha1/index.d.ts +5 -0
  33. package/dist/gs/crypto/sha1/index.js +103 -0
  34. package/dist/gs/crypto/sha1/index.js.map +1 -0
  35. package/dist/gs/crypto/sha256/index.js +2 -5
  36. package/dist/gs/crypto/sha256/index.js.map +1 -1
  37. package/dist/gs/crypto/sha512/index.js +2 -5
  38. package/dist/gs/crypto/sha512/index.js.map +1 -1
  39. package/dist/gs/embed/index.d.ts +6 -0
  40. package/dist/gs/embed/index.js +210 -5
  41. package/dist/gs/embed/index.js.map +1 -1
  42. package/dist/gs/fmt/fmt.d.ts +4 -4
  43. package/dist/gs/fmt/fmt.js +93 -19
  44. package/dist/gs/fmt/fmt.js.map +1 -1
  45. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +118 -6
  46. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +1 -1
  47. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.d.ts +45 -0
  48. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js +229 -0
  49. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js.map +1 -0
  50. package/dist/gs/io/fs/readdir.js +5 -3
  51. package/dist/gs/io/fs/readdir.js.map +1 -1
  52. package/dist/gs/io/io.d.ts +18 -11
  53. package/dist/gs/io/io.js +107 -44
  54. package/dist/gs/io/io.js.map +1 -1
  55. package/dist/gs/math/bits/index.d.ts +26 -5
  56. package/dist/gs/math/bits/index.js +13 -24
  57. package/dist/gs/math/bits/index.js.map +1 -1
  58. package/dist/gs/net/http/httptest/index.js +7 -5
  59. package/dist/gs/net/http/httptest/index.js.map +1 -1
  60. package/dist/gs/net/http/index.d.ts +11 -1
  61. package/dist/gs/net/http/index.js +157 -11
  62. package/dist/gs/net/http/index.js.map +1 -1
  63. package/dist/gs/os/types_js.gs.d.ts +6 -2
  64. package/dist/gs/os/types_js.gs.js +169 -8
  65. package/dist/gs/os/types_js.gs.js.map +1 -1
  66. package/dist/gs/os/zero_copy_posix.gs.js +1 -1
  67. package/dist/gs/os/zero_copy_posix.gs.js.map +1 -1
  68. package/dist/gs/reflect/type.d.ts +1 -0
  69. package/dist/gs/reflect/type.js +80 -51
  70. package/dist/gs/reflect/type.js.map +1 -1
  71. package/dist/gs/strings/reader.d.ts +1 -1
  72. package/dist/gs/strings/reader.js +2 -2
  73. package/dist/gs/strings/reader.js.map +1 -1
  74. package/dist/gs/sync/sync.d.ts +2 -1
  75. package/dist/gs/sync/sync.js +37 -16
  76. package/dist/gs/sync/sync.js.map +1 -1
  77. package/dist/gs/syscall/js/index.js +9 -0
  78. package/dist/gs/syscall/js/index.js.map +1 -1
  79. package/dist/gs/testing/testing.js +8 -6
  80. package/dist/gs/testing/testing.js.map +1 -1
  81. package/gs/builtin/builtin.ts +25 -2
  82. package/gs/builtin/channel.ts +47 -9
  83. package/gs/builtin/runtime-contract.test.ts +78 -0
  84. package/gs/builtin/slice.ts +7 -0
  85. package/gs/builtin/type.ts +97 -8
  86. package/gs/bytes/bytes.gs.ts +19 -10
  87. package/gs/bytes/bytes.test.ts +17 -0
  88. package/gs/compress/zlib/index.test.ts +97 -0
  89. package/gs/compress/zlib/index.ts +117 -27
  90. package/gs/compress/zlib/meta.json +4 -1
  91. package/gs/context/context.test.ts +5 -1
  92. package/gs/crypto/sha1/index.test.ts +45 -0
  93. package/gs/crypto/sha1/index.ts +127 -0
  94. package/gs/crypto/sha1/meta.json +8 -0
  95. package/gs/crypto/sha256/index.test.ts +14 -2
  96. package/gs/crypto/sha256/index.ts +3 -6
  97. package/gs/crypto/sha512/index.test.ts +17 -2
  98. package/gs/crypto/sha512/index.ts +3 -6
  99. package/gs/embed/index.test.ts +87 -0
  100. package/gs/embed/index.ts +229 -5
  101. package/gs/fmt/fmt.test.ts +61 -3
  102. package/gs/fmt/fmt.ts +115 -22
  103. package/gs/fmt/meta.json +6 -1
  104. package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +8 -1
  105. package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +139 -11
  106. package/gs/github.com/aperturerobotics/util/conc/index.test.ts +1 -1
  107. package/gs/github.com/go-git/go-billy/v6/osfs/index.test.ts +110 -0
  108. package/gs/github.com/go-git/go-billy/v6/osfs/index.ts +280 -0
  109. package/gs/github.com/go-git/go-billy/v6/osfs/meta.json +8 -0
  110. package/gs/io/fs/readdir.test.ts +38 -0
  111. package/gs/io/fs/readdir.ts +7 -3
  112. package/gs/io/io.test.ts +135 -0
  113. package/gs/io/io.ts +143 -63
  114. package/gs/io/meta.json +7 -1
  115. package/gs/math/bits/index.ts +52 -28
  116. package/gs/net/http/httptest/index.test.ts +34 -2
  117. package/gs/net/http/httptest/index.ts +23 -8
  118. package/gs/net/http/index.test.ts +46 -0
  119. package/gs/net/http/index.ts +178 -12
  120. package/gs/os/file_unix_js.test.ts +52 -0
  121. package/gs/os/meta.json +4 -0
  122. package/gs/os/readdir.test.ts +56 -0
  123. package/gs/os/types_js.gs.ts +169 -8
  124. package/gs/os/zero_copy_posix.gs.ts +1 -2
  125. package/gs/reflect/deepequal.test.ts +10 -1
  126. package/gs/reflect/type.ts +91 -56
  127. package/gs/reflect/typefor.test.ts +31 -1
  128. package/gs/strings/meta.json +5 -2
  129. package/gs/strings/reader.test.ts +2 -2
  130. package/gs/strings/reader.ts +2 -2
  131. package/gs/sync/meta.json +2 -0
  132. package/gs/sync/sync.test.ts +41 -1
  133. package/gs/sync/sync.ts +41 -16
  134. package/gs/syscall/js/index.test.ts +18 -0
  135. package/gs/syscall/js/index.ts +12 -0
  136. package/gs/testing/testing.test.ts +32 -3
  137. package/gs/testing/testing.ts +13 -10
  138. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { describe, expect, it } from 'vitest'
3
+ import * as $ from '@goscript/builtin/index.js'
3
4
 
4
- import { New, Sum384, Sum512, Sum512_224, Sum512_256 } from './index.js'
5
+ import { New, Size, Sum384, Sum512, Sum512_224, Sum512_256 } from './index.js'
5
6
 
6
7
  describe('crypto/sha512 override', () => {
7
8
  it('matches Node digests', async () => {
@@ -18,7 +19,21 @@ describe('crypto/sha512 override', () => {
18
19
  h.Write(new TextEncoder().encode('go'))
19
20
  h.Write(new TextEncoder().encode('script'))
20
21
 
21
- expect(toHex(await h.Sum(null))).toBe(nodeHash('sha512', new TextEncoder().encode('goscript')))
22
+ expect(toHex(await h.Sum(null))).toBe(
23
+ nodeHash('sha512', new TextEncoder().encode('goscript')),
24
+ )
25
+ })
26
+
27
+ it('appends into spare byte-slice backing', async () => {
28
+ const h = New()
29
+ h.Write(new TextEncoder().encode('abc'))
30
+
31
+ const backing = $.makeSlice<number>(Size, undefined, 'byte')
32
+ const out = await h.Sum($.goSlice(backing, 0, 0))
33
+ expect(out.length).toBe(Size)
34
+ expect(toHex($.bytesToUint8Array(backing))).toBe(
35
+ nodeHash('sha512', new TextEncoder().encode('abc')),
36
+ )
22
37
  })
23
38
  })
24
39
 
@@ -38,7 +38,7 @@ class Digest {
38
38
  this.canCopyHash ?
39
39
  new Uint8Array(this.hash!.copy!().digest())
40
40
  : await sum(this.algorithm, this.snapshotBytes())
41
- return appendDigest($.bytesToUint8Array(b), digest)
41
+ return appendDigest(b, digest)
42
42
  }
43
43
 
44
44
  Reset(): void {
@@ -123,11 +123,8 @@ async function sum(
123
123
  return new Uint8Array(digest)
124
124
  }
125
125
 
126
- function appendDigest(prefix: Uint8Array, digest: Uint8Array): Uint8Array {
127
- const out = new Uint8Array(prefix.length + digest.length)
128
- out.set(prefix)
129
- out.set(digest, prefix.length)
130
- return out
126
+ function appendDigest(prefix: $.Bytes, digest: Uint8Array): $.Bytes {
127
+ return $.append(prefix as any, ...digest) as $.Bytes
131
128
  }
132
129
 
133
130
  function createNodeHash(algorithm: ShaAlgorithm): NodeCryptoHash | null {
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { cloneStructValue, markAsStructValue } from '@goscript/builtin/index.js'
4
+ import { EOF } from '@goscript/io/index.js'
5
+ import { ReadDir, ReadFile, Stat } from '@goscript/io/fs/index.js'
6
+
7
+ import { FS } from './index.js'
8
+
9
+ describe('embed.FS', () => {
10
+ it('clones embedded files as Go struct values', () => {
11
+ const original = markAsStructValue(
12
+ new FS(new Map([['config-set.bin', new Uint8Array([1, 2, 3])]])),
13
+ )
14
+
15
+ const cloned = cloneStructValue(original)
16
+ const [data, err] = cloned.ReadFile('config-set.bin')
17
+
18
+ expect(err).toBeNull()
19
+ expect(Array.from(data)).toEqual([1, 2, 3])
20
+ })
21
+
22
+ it('supports io/fs read, stat, and directory APIs', () => {
23
+ const fsys = markAsStructValue(
24
+ new FS(
25
+ new Map([
26
+ ['config-set.bin', new Uint8Array([1, 2, 3])],
27
+ ['assets/config.json', new Uint8Array([4])],
28
+ ]),
29
+ ),
30
+ )
31
+
32
+ const [data, readErr] = ReadFile(fsys, 'config-set.bin')
33
+ expect(readErr).toBeNull()
34
+ expect(Array.from(data)).toEqual([1, 2, 3])
35
+ data[0] = 9
36
+ const [dataAgain, readAgainErr] = ReadFile(fsys, 'config-set.bin')
37
+ expect(readAgainErr).toBeNull()
38
+ expect(Array.from(dataAgain)).toEqual([1, 2, 3])
39
+
40
+ const [rootEntries, rootErr] = ReadDir(fsys, '.')
41
+ expect(rootErr).toBeNull()
42
+ expect(rootEntries!.map((entry) => entry!.Name())).toEqual([
43
+ 'assets',
44
+ 'config-set.bin',
45
+ ])
46
+
47
+ const [assetInfo, statErr] = Stat(fsys, 'assets')
48
+ expect(statErr).toBeNull()
49
+ expect(assetInfo!.IsDir()).toBe(true)
50
+
51
+ const [assetEntries, assetErr] = ReadDir(fsys, 'assets')
52
+ expect(assetErr).toBeNull()
53
+ expect(assetEntries!.map((entry) => entry!.Name())).toEqual(['config.json'])
54
+ })
55
+
56
+ it('supports Open file reads and directory iteration', () => {
57
+ const fsys = markAsStructValue(
58
+ new FS(
59
+ new Map([
60
+ ['config-set.bin', new Uint8Array([1, 2, 3])],
61
+ ['assets/config.json', new Uint8Array([4])],
62
+ ]),
63
+ ),
64
+ )
65
+
66
+ const [file, openErr] = fsys.Open('config-set.bin')
67
+ expect(openErr).toBeNull()
68
+ const buffer = new Uint8Array(2)
69
+ const [firstRead, firstErr] = file!.Read(buffer)
70
+ expect(firstErr).toBeNull()
71
+ expect(firstRead).toBe(2)
72
+ expect(Array.from(buffer)).toEqual([1, 2])
73
+ const [secondRead, secondErr] = file!.Read(buffer)
74
+ expect(secondErr).toBeNull()
75
+ expect(secondRead).toBe(1)
76
+ expect(Array.from(buffer)).toEqual([3, 2])
77
+ const [eofRead, eofErr] = file!.Read(buffer)
78
+ expect(eofRead).toBe(0)
79
+ expect(eofErr).toBe(EOF)
80
+
81
+ const [dir, dirOpenErr] = fsys.Open('.')
82
+ expect(dirOpenErr).toBeNull()
83
+ const [entries, readDirErr] = dir!.ReadDir(1)
84
+ expect(readDirErr).toBeNull()
85
+ expect(entries!.map((entry) => entry!.Name())).toEqual(['assets'])
86
+ })
87
+ })
package/gs/embed/index.ts CHANGED
@@ -1,20 +1,244 @@
1
1
  import * as $ from '@goscript/builtin/index.js'
2
+ import * as io from '@goscript/io/index.js'
2
3
  import * as fs from '@goscript/io/fs/index.js'
4
+ import * as time from '@goscript/time/index.js'
3
5
 
4
6
  export class FS {
7
+ private files: Map<string, Uint8Array>
8
+
9
+ constructor(files?: Map<string, Uint8Array>) {
10
+ this.files = files ?? new Map()
11
+ }
12
+
13
+ clone(): FS {
14
+ const files = new Map<string, Uint8Array>()
15
+ for (const [name, data] of this.files) {
16
+ files.set(name, data.slice())
17
+ }
18
+ return $.markAsStructValue(new FS(files))
19
+ }
20
+
5
21
  Open(name: string): [fs.File, $.GoError] {
6
- return [null, pathError('open', name)]
22
+ const err = validatePath('open', name)
23
+ if (err != null) {
24
+ return [null, err]
25
+ }
26
+ const data = this.files.get(name)
27
+ if (data !== undefined) {
28
+ return [new embedFile(name, data), null]
29
+ }
30
+ const entries = this.dirEntries(name)
31
+ if (entries === null) {
32
+ return [null, pathError('open', name, fs.ErrNotExist)]
33
+ }
34
+ return [new embedFile(name, null, entries), null]
7
35
  }
8
36
 
9
37
  ReadDir(name: string): [$.Slice<fs.DirEntry>, $.GoError] {
10
- return [null, pathError('read', name)]
38
+ const err = validatePath('read', name)
39
+ if (err != null) {
40
+ return [null, err]
41
+ }
42
+ const entries = this.dirEntries(name)
43
+ if (entries === null) {
44
+ return [null, pathError('read', name, fs.ErrNotExist)]
45
+ }
46
+ return [entries, null]
11
47
  }
12
48
 
13
49
  ReadFile(name: string): [Uint8Array, $.GoError] {
14
- return [new Uint8Array(0), pathError('read', name)]
50
+ const err = validatePath('read', name)
51
+ if (err != null) {
52
+ return [new Uint8Array(0), err]
53
+ }
54
+ const data = this.files.get(name)
55
+ if (data === undefined) {
56
+ const err = this.dirExists(name) ? fs.ErrInvalid : fs.ErrNotExist
57
+ return [new Uint8Array(0), pathError('read', name, err)]
58
+ }
59
+ return [data.slice(), null]
60
+ }
61
+
62
+ Stat(name: string): [fs.FileInfo, $.GoError] {
63
+ const err = validatePath('stat', name)
64
+ if (err != null) {
65
+ return [null, err]
66
+ }
67
+ const data = this.files.get(name)
68
+ if (data !== undefined) {
69
+ return [new embedFileInfo(baseName(name), data.byteLength, 0o444), null]
70
+ }
71
+ if (!this.dirExists(name)) {
72
+ return [null, pathError('stat', name, fs.ErrNotExist)]
73
+ }
74
+ return [new embedFileInfo(baseName(name), 0, fs.ModeDir | 0o555), null]
75
+ }
76
+
77
+ private dirEntries(name: string): $.Slice<fs.DirEntry> | null {
78
+ if (!this.dirExists(name)) {
79
+ return null
80
+ }
81
+ const prefix = name === '.' ? '' : name + '/'
82
+ const entries = new Map<string, fs.DirEntry>()
83
+ for (const [filePath, data] of this.files) {
84
+ if (prefix !== '' && !filePath.startsWith(prefix)) {
85
+ continue
86
+ }
87
+ const rest = prefix === '' ? filePath : filePath.slice(prefix.length)
88
+ if (rest === '') {
89
+ continue
90
+ }
91
+ const slash = rest.indexOf('/')
92
+ const childName = slash === -1 ? rest : rest.slice(0, slash)
93
+ if (entries.has(childName)) {
94
+ continue
95
+ }
96
+ const isDir = slash !== -1
97
+ const info =
98
+ isDir ?
99
+ new embedFileInfo(childName, 0, fs.ModeDir | 0o555)
100
+ : new embedFileInfo(childName, data.byteLength, 0o444)
101
+ entries.set(childName, new embedDirEntry(info))
102
+ }
103
+ return Array.from(entries.values()).sort((a, b) =>
104
+ a!.Name().localeCompare(b!.Name()),
105
+ )
106
+ }
107
+
108
+ private dirExists(name: string): boolean {
109
+ if (name === '.') {
110
+ return true
111
+ }
112
+ const prefix = name + '/'
113
+ for (const path of this.files.keys()) {
114
+ if (path.startsWith(prefix)) {
115
+ return true
116
+ }
117
+ }
118
+ return false
119
+ }
120
+ }
121
+
122
+ class embedFile {
123
+ private offset = 0
124
+ private dirOffset = 0
125
+
126
+ constructor(
127
+ private readonly name: string,
128
+ private readonly data: Uint8Array | null,
129
+ private readonly entries: $.Slice<fs.DirEntry> = [],
130
+ ) {}
131
+
132
+ Close(): $.GoError {
133
+ return null
134
+ }
135
+
136
+ Read(buffer: Uint8Array): [number, $.GoError] {
137
+ if (this.data === null) {
138
+ return [0, pathError('read', this.name, fs.ErrInvalid)]
139
+ }
140
+ if (this.offset >= this.data.byteLength) {
141
+ return [0, io.EOF]
142
+ }
143
+ const n = Math.min(buffer.byteLength, this.data.byteLength - this.offset)
144
+ buffer.set(this.data.subarray(this.offset, this.offset + n))
145
+ this.offset += n
146
+ return [n, null]
147
+ }
148
+
149
+ ReadDir(n: number): [$.Slice<fs.DirEntry>, $.GoError] {
150
+ if (this.data !== null) {
151
+ return [null, pathError('readdir', this.name, fs.ErrInvalid)]
152
+ }
153
+ const allEntries = this.entries ?? []
154
+ if (n <= 0) {
155
+ const entries = allEntries.slice(this.dirOffset)
156
+ this.dirOffset = allEntries.length
157
+ return [entries, null]
158
+ }
159
+ if (this.dirOffset >= allEntries.length) {
160
+ return [[], io.EOF]
161
+ }
162
+ const entries = allEntries.slice(this.dirOffset, this.dirOffset + n)
163
+ this.dirOffset += entries.length
164
+ return [entries, null]
165
+ }
166
+
167
+ Stat(): [fs.FileInfo, $.GoError] {
168
+ if (this.data === null) {
169
+ return [new embedFileInfo(baseName(this.name), 0, fs.ModeDir | 0o555), null]
170
+ }
171
+ return [new embedFileInfo(baseName(this.name), this.data.byteLength, 0o444), null]
172
+ }
173
+ }
174
+
175
+ class embedFileInfo {
176
+ constructor(
177
+ private readonly name: string,
178
+ private readonly size: number,
179
+ private readonly mode: fs.FileMode,
180
+ ) {}
181
+
182
+ IsDir(): boolean {
183
+ return fs.FileMode_IsDir(this.mode)
184
+ }
185
+
186
+ ModTime(): time.Time {
187
+ return new time.Time()
188
+ }
189
+
190
+ Mode(): fs.FileMode {
191
+ return this.mode
192
+ }
193
+
194
+ Name(): string {
195
+ return this.name
196
+ }
197
+
198
+ Size(): number {
199
+ return this.size
200
+ }
201
+
202
+ Sys(): null {
203
+ return null
204
+ }
205
+ }
206
+
207
+ class embedDirEntry {
208
+ constructor(private readonly info: fs.FileInfo) {}
209
+
210
+ Info(): [fs.FileInfo, $.GoError] {
211
+ return [this.info, null]
212
+ }
213
+
214
+ IsDir(): boolean {
215
+ return this.info!.IsDir()
216
+ }
217
+
218
+ Name(): string {
219
+ return this.info!.Name()
220
+ }
221
+
222
+ Type(): fs.FileMode {
223
+ return fs.fileModeType(this.info!.Mode())
15
224
  }
16
225
  }
17
226
 
18
- function pathError(op: string, name: string): $.GoError {
19
- return new fs.PathError({ Op: op, Path: name, Err: fs.ErrNotExist })
227
+ function validatePath(op: string, name: string): $.GoError {
228
+ if (!fs.ValidPath(name)) {
229
+ return pathError(op, name, fs.ErrInvalid)
230
+ }
231
+ return null
232
+ }
233
+
234
+ function pathError(op: string, name: string, err: $.GoError): $.GoError {
235
+ return new fs.PathError({ Op: op, Path: name, Err: err })
236
+ }
237
+
238
+ function baseName(name: string): string {
239
+ const idx = name.lastIndexOf('/')
240
+ if (idx === -1) {
241
+ return name
242
+ }
243
+ return name.slice(idx + 1)
20
244
  }
@@ -1,5 +1,6 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest'
2
2
  import { resetHostRuntimeForTests } from '@goscript/builtin/hostio.js'
3
+ import * as $ from '@goscript/builtin/index.js'
3
4
  import * as fmt from './fmt.js'
4
5
 
5
6
  const originalDeno = (globalThis as any).Deno
@@ -57,6 +58,15 @@ describe('fmt basic value formatting', () => {
57
58
  expect(fmt.Sprintf('Type: %T', 3.14)).toBe('Type: float64')
58
59
  expect(fmt.Sprintf('Type: %T', 'hello')).toBe('Type: string')
59
60
  expect(fmt.Sprintf('Type: %T', true)).toBe('Type: bool')
61
+ expect(
62
+ fmt.Sprintf(
63
+ 'Type: %T',
64
+ $.namedValueInterfaceValue(123, 'int', {}, {
65
+ kind: $.TypeKind.Basic,
66
+ name: 'int',
67
+ }),
68
+ ),
69
+ ).toBe('Type: int')
60
70
  })
61
71
 
62
72
  it('%d truncation behavior including negatives', () => {
@@ -114,6 +124,20 @@ describe('fmt basic value formatting', () => {
114
124
  // We prefer GoString() first
115
125
  expect(fmt.Sprintf('%v', goStringer)).toBe('<go stringer>')
116
126
  })
127
+
128
+ it('%w formats errors by Error method', () => {
129
+ const err = $.newError('root')
130
+ expect(fmt.Errorf('wrap: %w', err)?.Error()).toBe('wrap: root')
131
+ })
132
+
133
+ it('%s formats stringers by String method', () => {
134
+ const stringer = {
135
+ String() {
136
+ return 'string-value'
137
+ },
138
+ }
139
+ expect(fmt.Sprintf('value=%s', stringer)).toBe('value=string-value')
140
+ })
117
141
  })
118
142
 
119
143
  describe('fmt spacing rules', () => {
@@ -145,7 +169,7 @@ describe('fmt spacing rules', () => {
145
169
  expect(output).toBe('hi there 1 2\n')
146
170
  })
147
171
 
148
- it('Fprint/Fprintln behave like Print/Println with writers', () => {
172
+ it('Fprint/Fprintln behave like Print/Println with writers', async () => {
149
173
  const chunks: Uint8Array[] = []
150
174
  const writer = {
151
175
  Write(b: Uint8Array): [number, any] {
@@ -154,14 +178,29 @@ describe('fmt spacing rules', () => {
154
178
  },
155
179
  }
156
180
 
157
- let [n, err] = fmt.Fprint(writer, 1, 2, 'x', 3)
181
+ let [n, err] = await fmt.Fprint(writer, 1, 2, 'x', 3)
158
182
  expect(err).toBeNull()
159
183
  expect(n).toBe(5) // "1 2x3".length
160
184
  expect(new TextDecoder().decode(chunks[0])).toBe('1 2x3')
161
- ;[, err] = fmt.Fprintln(writer, 'hi', 'there', 1, 2)
185
+ ;[, err] = await fmt.Fprintln(writer, 'hi', 'there', 1, 2)
162
186
  expect(err).toBeNull()
163
187
  expect(new TextDecoder().decode(chunks[1])).toBe('hi there 1 2\n')
164
188
  })
189
+
190
+ it('Fprintf awaits async writers', async () => {
191
+ const chunks: Uint8Array[] = []
192
+ const writer = {
193
+ async Write(b: Uint8Array): Promise<[number, any]> {
194
+ chunks.push(b)
195
+ return [b.length, null]
196
+ },
197
+ }
198
+
199
+ const [n, err] = await fmt.Fprintf(writer, 'n=%d s=%s', 7, 'ok')
200
+ expect(err).toBeNull()
201
+ expect(n).toBe(8)
202
+ expect(new TextDecoder().decode(chunks[0])).toBe('n=7 s=ok')
203
+ })
165
204
  })
166
205
 
167
206
  describe('fmt parseFormat basic cases', () => {
@@ -187,3 +226,22 @@ describe('fmt parseFormat basic cases', () => {
187
226
  expect(fmt.Sprintf('%c', 65)).toBe('A')
188
227
  })
189
228
  })
229
+
230
+ describe('fmt scanning', () => {
231
+ it('scans decimal fields separated by literals', () => {
232
+ const start = $.varRef(0)
233
+ const end = $.varRef(0)
234
+
235
+ const [n, err] = fmt.Sscanf(
236
+ 'bytes=12-34',
237
+ 'bytes=%d-%d',
238
+ $.interfaceValue(start, '*int64'),
239
+ $.interfaceValue(end, '*int64'),
240
+ )
241
+
242
+ expect(err).toBeNull()
243
+ expect(n).toBe(2)
244
+ expect(start.value).toBe(12)
245
+ expect(end.value).toBe(34)
246
+ })
247
+ })
package/gs/fmt/fmt.ts CHANGED
@@ -34,15 +34,20 @@ function formatValue(value: any, verb: string): string {
34
34
  switch (verb) {
35
35
  case 'v': // default format
36
36
  return defaultFormat(value)
37
+ case 'w': // wrapped error
38
+ return defaultFormat(value)
37
39
  case 'd': // decimal integer
38
40
  return String(Math.trunc(Number(value)))
39
41
  case 'f': // decimal point, no exponent
40
42
  return Number(value).toString()
41
43
  case 's': // string
42
- return String(value)
44
+ if (typeof value === 'string') return value
45
+ if (value instanceof Uint8Array) return $.bytesToString(value)
46
+ return defaultFormat(value)
43
47
  case 't': // boolean
44
48
  return value ? 'true' : 'false'
45
49
  case 'T': // type (approximate Go names for primitives we need)
50
+ if (hasGoTypeName(value)) return value.__goType
46
51
  if (typeof value === 'number') {
47
52
  return Number.isInteger(value) ? 'int' : 'float64'
48
53
  }
@@ -85,6 +90,14 @@ function formatValue(value: any, verb: string): string {
85
90
  }
86
91
  }
87
92
 
93
+ function hasGoTypeName(value: unknown): value is { __goType: string } {
94
+ return (
95
+ value !== null &&
96
+ typeof value === 'object' &&
97
+ typeof (value as { __goType?: unknown }).__goType === 'string'
98
+ )
99
+ }
100
+
88
101
  function defaultFormat(value: any): string {
89
102
  if (value === null || value === undefined) return '<nil>'
90
103
  if (typeof value === 'boolean') return value ? 'true' : 'false'
@@ -120,6 +133,9 @@ function defaultFormat(value: any): string {
120
133
  // Ignore error by continuing to next case.
121
134
  }
122
135
  }
136
+ if ('__goValue' in value) {
137
+ return defaultFormat((value as { __goValue: unknown }).__goValue)
138
+ }
123
139
  // Basic Map/Set rendering
124
140
  if (value instanceof Map) {
125
141
  const parts: string[] = []
@@ -357,8 +373,21 @@ export function Sprintln(...a: any[]): string {
357
373
  return a.map(defaultFormat).join(' ') + '\n'
358
374
  }
359
375
 
376
+ async function writeToWriter(
377
+ w: any,
378
+ out: string,
379
+ ): Promise<[number, $.GoError | null]> {
380
+ if (w && w.Write) {
381
+ return await w.Write(new TextEncoder().encode(out))
382
+ }
383
+ return [0, $.newError('Writer does not implement Write method')]
384
+ }
385
+
360
386
  // Fprint functions (write to Writer) - simplified implementation
361
- export function Fprint(w: any, ...a: any[]): [number, $.GoError | null] {
387
+ export async function Fprint(
388
+ w: any,
389
+ ...a: any[]
390
+ ): Promise<[number, $.GoError | null]> {
362
391
  // Same spacing as Print
363
392
  let out = ''
364
393
  for (let i = 0; i < a.length; i++) {
@@ -369,32 +398,26 @@ export function Fprint(w: any, ...a: any[]): [number, $.GoError | null] {
369
398
  }
370
399
  out += defaultFormat(a[i])
371
400
  }
372
- if (w && w.Write) {
373
- return w.Write(new TextEncoder().encode(out))
374
- }
375
- return [0, $.newError('Writer does not implement Write method')]
401
+ return await writeToWriter(w, out)
376
402
  }
377
403
 
378
- export function Fprintf(
404
+ export async function Fprintf(
379
405
  w: any,
380
406
  format: string,
381
407
  ...a: any[]
382
- ): [number, $.GoError | null] {
408
+ ): Promise<[number, $.GoError | null]> {
383
409
  const result = parseFormat(format, a)
384
- if (w && w.Write) {
385
- return w.Write(new TextEncoder().encode(result))
386
- }
387
- return [0, $.newError('Writer does not implement Write method')]
410
+ return await writeToWriter(w, result)
388
411
  }
389
412
 
390
- export function Fprintln(w: any, ...a: any[]): [number, $.GoError | null] {
413
+ export async function Fprintln(
414
+ w: any,
415
+ ...a: any[]
416
+ ): Promise<[number, $.GoError | null]> {
391
417
  // Same behavior as Println
392
418
  const body = a.map(defaultFormat).join(' ')
393
419
  const result = body + '\n'
394
- if (w && w.Write) {
395
- return w.Write(new TextEncoder().encode(result))
396
- }
397
- return [0, $.newError('Writer does not implement Write method')]
420
+ return await writeToWriter(w, result)
398
421
  }
399
422
 
400
423
  // Append functions (append to byte slice)
@@ -488,12 +511,82 @@ export function Sscan(_str: string, ..._a: any[]): [number, $.GoError | null] {
488
511
  }
489
512
 
490
513
  export function Sscanf(
491
- _str: string,
492
- _format: string,
493
- ..._a: any[]
514
+ str: string,
515
+ format: string,
516
+ ...a: any[]
494
517
  ): [number, $.GoError | null] {
495
- // TODO: Implement formatted scanning from string
496
- return [0, $.newError('Sscanf not implemented')]
518
+ const parts = buildScanPattern(format)
519
+ if (parts == null) {
520
+ return [0, $.newError(`unsupported Sscanf format: ${format}`)]
521
+ }
522
+ const match = parts.pattern.exec(str)
523
+ if (match == null) {
524
+ return [0, $.newError('input does not match format')]
525
+ }
526
+
527
+ let assigned = 0
528
+ for (let i = 0; i < parts.verbs.length && i < a.length; i++) {
529
+ const raw = match[i + 1]
530
+ const value = parts.verbs[i] === 'd' ? Number.parseInt(raw, 10) : raw
531
+ if (!assignScanValue(a[i], value)) {
532
+ return [assigned, $.newError('scan destination is not assignable')]
533
+ }
534
+ assigned++
535
+ }
536
+ return [assigned, null]
537
+ }
538
+
539
+ function buildScanPattern(
540
+ format: string,
541
+ ): { pattern: RegExp; verbs: string[] } | null {
542
+ let source = '^'
543
+ const verbs: string[] = []
544
+ for (let i = 0; i < format.length; i++) {
545
+ const ch = format[i]
546
+ if (ch !== '%') {
547
+ source += /\s/.test(ch) ? '\\s+' : escapeRegExp(ch)
548
+ continue
549
+ }
550
+ const verb = format[++i]
551
+ if (verb === '%') {
552
+ source += '%'
553
+ continue
554
+ }
555
+ if (verb === 'd') {
556
+ source += '([+-]?\\d+)'
557
+ verbs.push(verb)
558
+ continue
559
+ }
560
+ if (verb === 's') {
561
+ source += '(\\S+)'
562
+ verbs.push(verb)
563
+ continue
564
+ }
565
+ return null
566
+ }
567
+ source += '$'
568
+ return { pattern: new RegExp(source), verbs }
569
+ }
570
+
571
+ function assignScanValue(target: any, value: string | number): boolean {
572
+ const ref =
573
+ $.isVarRef(target) ? target
574
+ : (
575
+ target != null &&
576
+ typeof target === 'object' &&
577
+ $.isVarRef(target.__goValue)
578
+ ) ?
579
+ target.__goValue
580
+ : null
581
+ if (ref == null) {
582
+ return false
583
+ }
584
+ ref.value = value
585
+ return true
586
+ }
587
+
588
+ function escapeRegExp(ch: string): string {
589
+ return ch.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
497
590
  }
498
591
 
499
592
  export function Sscanln(