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.
- package/cmd/goscript/cmd_compile.go +28 -8
- package/cmd/goscript/cmd_compile_test.go +105 -6
- package/compiler/build-flags.go +9 -10
- package/compiler/gotest/runner_test.go +127 -0
- package/compiler/lowered-program.go +1 -0
- package/compiler/lowering.go +1325 -194
- package/compiler/lowering_bench_test.go +350 -0
- package/compiler/override-registry_test.go +43 -0
- package/compiler/package-graph.go +61 -4
- package/compiler/package-graph_test.go +30 -0
- package/compiler/semantic-model-types.go +8 -0
- package/compiler/semantic-model.go +447 -22
- package/compiler/semantic-model_test.go +138 -0
- package/compiler/skeleton_test.go +1436 -50
- package/compiler/typescript-emitter.go +47 -4
- package/dist/gs/builtin/builtin.d.ts +2 -2
- package/dist/gs/builtin/builtin.js +20 -0
- package/dist/gs/builtin/builtin.js.map +1 -1
- package/dist/gs/builtin/channel.js +36 -9
- package/dist/gs/builtin/channel.js.map +1 -1
- package/dist/gs/builtin/slice.js +5 -0
- package/dist/gs/builtin/slice.js.map +1 -1
- package/dist/gs/builtin/type.d.ts +1 -1
- package/dist/gs/builtin/type.js +80 -8
- package/dist/gs/builtin/type.js.map +1 -1
- package/dist/gs/bytes/bytes.gs.d.ts +7 -5
- package/dist/gs/bytes/bytes.gs.js +10 -4
- package/dist/gs/bytes/bytes.gs.js.map +1 -1
- package/dist/gs/compress/zlib/index.d.ts +3 -3
- package/dist/gs/compress/zlib/index.js +88 -26
- package/dist/gs/compress/zlib/index.js.map +1 -1
- package/dist/gs/crypto/sha1/index.d.ts +5 -0
- package/dist/gs/crypto/sha1/index.js +103 -0
- package/dist/gs/crypto/sha1/index.js.map +1 -0
- package/dist/gs/crypto/sha256/index.js +2 -5
- package/dist/gs/crypto/sha256/index.js.map +1 -1
- package/dist/gs/crypto/sha512/index.js +2 -5
- package/dist/gs/crypto/sha512/index.js.map +1 -1
- package/dist/gs/embed/index.d.ts +6 -0
- package/dist/gs/embed/index.js +210 -5
- package/dist/gs/embed/index.js.map +1 -1
- package/dist/gs/fmt/fmt.d.ts +4 -4
- package/dist/gs/fmt/fmt.js +93 -19
- package/dist/gs/fmt/fmt.js.map +1 -1
- package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +118 -6
- package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +1 -1
- package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.d.ts +45 -0
- package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js +229 -0
- package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js.map +1 -0
- package/dist/gs/io/fs/readdir.js +5 -3
- package/dist/gs/io/fs/readdir.js.map +1 -1
- package/dist/gs/io/io.d.ts +18 -11
- package/dist/gs/io/io.js +107 -44
- package/dist/gs/io/io.js.map +1 -1
- package/dist/gs/math/bits/index.d.ts +26 -5
- package/dist/gs/math/bits/index.js +13 -24
- package/dist/gs/math/bits/index.js.map +1 -1
- package/dist/gs/net/http/httptest/index.js +7 -5
- package/dist/gs/net/http/httptest/index.js.map +1 -1
- package/dist/gs/net/http/index.d.ts +11 -1
- package/dist/gs/net/http/index.js +157 -11
- package/dist/gs/net/http/index.js.map +1 -1
- package/dist/gs/os/types_js.gs.d.ts +6 -2
- package/dist/gs/os/types_js.gs.js +169 -8
- package/dist/gs/os/types_js.gs.js.map +1 -1
- package/dist/gs/os/zero_copy_posix.gs.js +1 -1
- package/dist/gs/os/zero_copy_posix.gs.js.map +1 -1
- package/dist/gs/reflect/type.d.ts +1 -0
- package/dist/gs/reflect/type.js +80 -51
- package/dist/gs/reflect/type.js.map +1 -1
- package/dist/gs/strings/reader.d.ts +1 -1
- package/dist/gs/strings/reader.js +2 -2
- package/dist/gs/strings/reader.js.map +1 -1
- package/dist/gs/sync/sync.d.ts +2 -1
- package/dist/gs/sync/sync.js +37 -16
- package/dist/gs/sync/sync.js.map +1 -1
- package/dist/gs/syscall/js/index.js +9 -0
- package/dist/gs/syscall/js/index.js.map +1 -1
- package/dist/gs/testing/testing.js +8 -6
- package/dist/gs/testing/testing.js.map +1 -1
- package/gs/builtin/builtin.ts +25 -2
- package/gs/builtin/channel.ts +47 -9
- package/gs/builtin/runtime-contract.test.ts +78 -0
- package/gs/builtin/slice.ts +7 -0
- package/gs/builtin/type.ts +97 -8
- package/gs/bytes/bytes.gs.ts +19 -10
- package/gs/bytes/bytes.test.ts +17 -0
- package/gs/compress/zlib/index.test.ts +97 -0
- package/gs/compress/zlib/index.ts +117 -27
- package/gs/compress/zlib/meta.json +4 -1
- package/gs/context/context.test.ts +5 -1
- package/gs/crypto/sha1/index.test.ts +45 -0
- package/gs/crypto/sha1/index.ts +127 -0
- package/gs/crypto/sha1/meta.json +8 -0
- package/gs/crypto/sha256/index.test.ts +14 -2
- package/gs/crypto/sha256/index.ts +3 -6
- package/gs/crypto/sha512/index.test.ts +17 -2
- package/gs/crypto/sha512/index.ts +3 -6
- package/gs/embed/index.test.ts +87 -0
- package/gs/embed/index.ts +229 -5
- package/gs/fmt/fmt.test.ts +61 -3
- package/gs/fmt/fmt.ts +115 -22
- package/gs/fmt/meta.json +6 -1
- package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +8 -1
- package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +139 -11
- package/gs/github.com/aperturerobotics/util/conc/index.test.ts +1 -1
- package/gs/github.com/go-git/go-billy/v6/osfs/index.test.ts +110 -0
- package/gs/github.com/go-git/go-billy/v6/osfs/index.ts +280 -0
- package/gs/github.com/go-git/go-billy/v6/osfs/meta.json +8 -0
- package/gs/io/fs/readdir.test.ts +38 -0
- package/gs/io/fs/readdir.ts +7 -3
- package/gs/io/io.test.ts +135 -0
- package/gs/io/io.ts +143 -63
- package/gs/io/meta.json +7 -1
- package/gs/math/bits/index.ts +52 -28
- package/gs/net/http/httptest/index.test.ts +34 -2
- package/gs/net/http/httptest/index.ts +23 -8
- package/gs/net/http/index.test.ts +46 -0
- package/gs/net/http/index.ts +178 -12
- package/gs/os/file_unix_js.test.ts +52 -0
- package/gs/os/meta.json +4 -0
- package/gs/os/readdir.test.ts +56 -0
- package/gs/os/types_js.gs.ts +169 -8
- package/gs/os/zero_copy_posix.gs.ts +1 -2
- package/gs/reflect/deepequal.test.ts +10 -1
- package/gs/reflect/type.ts +91 -56
- package/gs/reflect/typefor.test.ts +31 -1
- package/gs/strings/meta.json +5 -2
- package/gs/strings/reader.test.ts +2 -2
- package/gs/strings/reader.ts +2 -2
- package/gs/sync/meta.json +2 -0
- package/gs/sync/sync.test.ts +41 -1
- package/gs/sync/sync.ts +41 -16
- package/gs/syscall/js/index.test.ts +18 -0
- package/gs/syscall/js/index.ts +12 -0
- package/gs/testing/testing.test.ts +32 -3
- package/gs/testing/testing.ts +13 -10
- 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(
|
|
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(
|
|
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:
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
19
|
-
|
|
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
|
}
|
package/gs/fmt/fmt.test.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
...
|
|
514
|
+
str: string,
|
|
515
|
+
format: string,
|
|
516
|
+
...a: any[]
|
|
494
517
|
): [number, $.GoError | null] {
|
|
495
|
-
|
|
496
|
-
|
|
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(
|