goscript 0.1.3 → 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/lowering.go +596 -136
- package/compiler/lowering_bench_test.go +350 -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 +948 -14
- package/compiler/typescript-emitter.go +19 -2
- 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/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 +72 -5
- package/dist/gs/builtin/type.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.js +2 -5
- package/dist/gs/crypto/sha1/index.js.map +1 -1
- 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 +3 -3
- package/dist/gs/fmt/fmt.js +29 -16
- 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 +10 -6
- package/dist/gs/io/io.js +87 -42
- 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/index.d.ts +3 -1
- package/dist/gs/net/http/index.js +18 -1
- 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/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/runtime-contract.test.ts +45 -0
- package/gs/builtin/slice.ts +7 -0
- package/gs/builtin/type.ts +85 -5
- 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/crypto/sha1/index.test.ts +19 -2
- package/gs/crypto/sha1/index.ts +3 -6
- 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 +41 -3
- package/gs/fmt/fmt.ts +40 -17
- 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/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 +77 -6
- package/gs/io/io.ts +114 -52
- package/gs/io/meta.json +7 -1
- package/gs/math/bits/index.ts +52 -28
- package/gs/net/http/index.test.ts +16 -0
- package/gs/net/http/index.ts +19 -2
- 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/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 +1 -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
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
|
@@ -58,6 +58,15 @@ describe('fmt basic value formatting', () => {
|
|
|
58
58
|
expect(fmt.Sprintf('Type: %T', 3.14)).toBe('Type: float64')
|
|
59
59
|
expect(fmt.Sprintf('Type: %T', 'hello')).toBe('Type: string')
|
|
60
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')
|
|
61
70
|
})
|
|
62
71
|
|
|
63
72
|
it('%d truncation behavior including negatives', () => {
|
|
@@ -115,6 +124,20 @@ describe('fmt basic value formatting', () => {
|
|
|
115
124
|
// We prefer GoString() first
|
|
116
125
|
expect(fmt.Sprintf('%v', goStringer)).toBe('<go stringer>')
|
|
117
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
|
+
})
|
|
118
141
|
})
|
|
119
142
|
|
|
120
143
|
describe('fmt spacing rules', () => {
|
|
@@ -146,7 +169,7 @@ describe('fmt spacing rules', () => {
|
|
|
146
169
|
expect(output).toBe('hi there 1 2\n')
|
|
147
170
|
})
|
|
148
171
|
|
|
149
|
-
it('Fprint/Fprintln behave like Print/Println with writers', () => {
|
|
172
|
+
it('Fprint/Fprintln behave like Print/Println with writers', async () => {
|
|
150
173
|
const chunks: Uint8Array[] = []
|
|
151
174
|
const writer = {
|
|
152
175
|
Write(b: Uint8Array): [number, any] {
|
|
@@ -155,14 +178,29 @@ describe('fmt spacing rules', () => {
|
|
|
155
178
|
},
|
|
156
179
|
}
|
|
157
180
|
|
|
158
|
-
let [n, err] = fmt.Fprint(writer, 1, 2, 'x', 3)
|
|
181
|
+
let [n, err] = await fmt.Fprint(writer, 1, 2, 'x', 3)
|
|
159
182
|
expect(err).toBeNull()
|
|
160
183
|
expect(n).toBe(5) // "1 2x3".length
|
|
161
184
|
expect(new TextDecoder().decode(chunks[0])).toBe('1 2x3')
|
|
162
|
-
;[, err] = fmt.Fprintln(writer, 'hi', 'there', 1, 2)
|
|
185
|
+
;[, err] = await fmt.Fprintln(writer, 'hi', 'there', 1, 2)
|
|
163
186
|
expect(err).toBeNull()
|
|
164
187
|
expect(new TextDecoder().decode(chunks[1])).toBe('hi there 1 2\n')
|
|
165
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
|
+
})
|
|
166
204
|
})
|
|
167
205
|
|
|
168
206
|
describe('fmt parseFormat basic cases', () => {
|
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)
|
package/gs/fmt/meta.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import * as context from '@goscript/context/index.js'
|
|
4
|
+
import * as $ from '@goscript/builtin/index.js'
|
|
4
5
|
|
|
5
|
-
import { MuxedConn, NewWebSocketConn } from './index.js'
|
|
6
|
+
import { MuxedConn, NewClientSet, NewPrefixClient, NewWebSocketConn } from './index.js'
|
|
6
7
|
|
|
7
8
|
describe('starpc/srpc override', () => {
|
|
8
9
|
it('wraps websocket connections with the same outbound direction as Go', () => {
|
|
@@ -28,4 +29,10 @@ describe('starpc/srpc override', () => {
|
|
|
28
29
|
expect(serverConn?.outbound).toBe(false)
|
|
29
30
|
expect(clientConn?.outbound).toBe(true)
|
|
30
31
|
})
|
|
32
|
+
|
|
33
|
+
it('registers prefixed clients as srpc.Client values', () => {
|
|
34
|
+
const client = NewPrefixClient(NewClientSet(null), ['plugin-host/'])
|
|
35
|
+
|
|
36
|
+
expect($.typeAssert(client, 'srpc.Client').ok).toBe(true)
|
|
37
|
+
})
|
|
31
38
|
})
|
|
@@ -115,6 +115,33 @@ export interface Client {
|
|
|
115
115
|
): Promise<[Stream | null, $.GoError]>
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
$.registerInterfaceType('srpc.Client', null, [
|
|
119
|
+
{
|
|
120
|
+
name: 'ExecCall',
|
|
121
|
+
args: [
|
|
122
|
+
{ name: 'ctx', type: 'context.Context' },
|
|
123
|
+
{ name: 'service', type: { kind: $.TypeKind.Basic, name: 'string' } },
|
|
124
|
+
{ name: 'method', type: { kind: $.TypeKind.Basic, name: 'string' } },
|
|
125
|
+
{ name: 'in', type: 'srpc.Message' },
|
|
126
|
+
{ name: 'out', type: 'srpc.Message' },
|
|
127
|
+
],
|
|
128
|
+
returns: [{ name: '_r0', type: 'error' }],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'NewStream',
|
|
132
|
+
args: [
|
|
133
|
+
{ name: 'ctx', type: 'context.Context' },
|
|
134
|
+
{ name: 'service', type: { kind: $.TypeKind.Basic, name: 'string' } },
|
|
135
|
+
{ name: 'method', type: { kind: $.TypeKind.Basic, name: 'string' } },
|
|
136
|
+
{ name: 'firstMsg', type: 'srpc.Message' },
|
|
137
|
+
],
|
|
138
|
+
returns: [
|
|
139
|
+
{ name: '_r0', type: 'srpc.Stream' },
|
|
140
|
+
{ name: '_r1', type: 'error' },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
])
|
|
144
|
+
|
|
118
145
|
export class ClientSet implements Client {
|
|
119
146
|
private clients: $.Slice<Client | null>
|
|
120
147
|
|
|
@@ -395,6 +422,84 @@ class memoryStream implements Stream {
|
|
|
395
422
|
}
|
|
396
423
|
}
|
|
397
424
|
|
|
425
|
+
class streamQueue {
|
|
426
|
+
private queue: (Message | null)[] = []
|
|
427
|
+
private waiters: ((msg: Message | null, err: $.GoError) => void)[] = []
|
|
428
|
+
private closed = false
|
|
429
|
+
|
|
430
|
+
public send(msg: Message | null): $.GoError {
|
|
431
|
+
if (this.closed) {
|
|
432
|
+
return ErrCompleted
|
|
433
|
+
}
|
|
434
|
+
const waiter = this.waiters.shift()
|
|
435
|
+
if (waiter != null) {
|
|
436
|
+
waiter(msg, null)
|
|
437
|
+
return null
|
|
438
|
+
}
|
|
439
|
+
this.queue.push(msg)
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
public recv(msg: Message | null): MaybePromise<$.GoError> {
|
|
444
|
+
const next = this.queue.shift()
|
|
445
|
+
if (next !== undefined) {
|
|
446
|
+
if (msg != null && next != null) {
|
|
447
|
+
Object.assign(msg, next)
|
|
448
|
+
}
|
|
449
|
+
return null
|
|
450
|
+
}
|
|
451
|
+
if (this.closed) {
|
|
452
|
+
return io.EOF
|
|
453
|
+
}
|
|
454
|
+
return new Promise<$.GoError>((resolve) => {
|
|
455
|
+
this.waiters.push((sent, err) => {
|
|
456
|
+
if (msg != null && sent != null) {
|
|
457
|
+
Object.assign(msg, sent)
|
|
458
|
+
}
|
|
459
|
+
resolve(err)
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
public close(): $.GoError {
|
|
465
|
+
this.closed = true
|
|
466
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
467
|
+
waiter(null, io.EOF)
|
|
468
|
+
}
|
|
469
|
+
return null
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
class pairedMemoryStream implements Stream {
|
|
474
|
+
constructor(
|
|
475
|
+
private ctx: context.Context,
|
|
476
|
+
private incoming: streamQueue,
|
|
477
|
+
private outgoing: streamQueue,
|
|
478
|
+
) {}
|
|
479
|
+
|
|
480
|
+
public Context(): context.Context {
|
|
481
|
+
return this.ctx
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
public MsgSend(msg: Message | null): $.GoError {
|
|
485
|
+
return this.outgoing.send(msg)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
public MsgRecv(msg: Message | null): MaybePromise<$.GoError> {
|
|
489
|
+
return this.incoming.recv(msg)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
public CloseSend(): $.GoError {
|
|
493
|
+
return this.outgoing.close()
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
public Close(): $.GoError {
|
|
497
|
+
const incomingErr = this.incoming.close()
|
|
498
|
+
const outgoingErr = this.outgoing.close()
|
|
499
|
+
return incomingErr ?? outgoingErr
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
398
503
|
class streamWithClose implements Stream {
|
|
399
504
|
constructor(
|
|
400
505
|
private stream: Stream,
|
|
@@ -738,7 +843,7 @@ class transportClient implements Client {
|
|
|
738
843
|
if (output != null) {
|
|
739
844
|
output.Reset()
|
|
740
845
|
}
|
|
741
|
-
return await writer?.Close() ?? null
|
|
846
|
+
return (await writer?.Close()) ?? null
|
|
742
847
|
}
|
|
743
848
|
|
|
744
849
|
public async NewStream(
|
|
@@ -792,7 +897,9 @@ export function NewClient(openStream: OpenStreamFunc | null): Client {
|
|
|
792
897
|
class invokerClient implements Client {
|
|
793
898
|
constructor(
|
|
794
899
|
private invoker: Invoker | null,
|
|
795
|
-
private contextFn:
|
|
900
|
+
private contextFn:
|
|
901
|
+
| ((ctx: context.Context) => context.Context)
|
|
902
|
+
| null = null,
|
|
796
903
|
) {}
|
|
797
904
|
|
|
798
905
|
public async ExecCall(
|
|
@@ -833,19 +940,36 @@ class invokerClient implements Client {
|
|
|
833
940
|
if (this.invoker == null) {
|
|
834
941
|
return [null, ErrNoAvailableClients]
|
|
835
942
|
}
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
943
|
+
const streamCtx = this.contextFn == null ? ctx : this.contextFn(ctx)
|
|
944
|
+
const clientInput = new streamQueue()
|
|
945
|
+
const serverInput = new streamQueue()
|
|
946
|
+
const clientStream = new pairedMemoryStream(
|
|
947
|
+
streamCtx,
|
|
948
|
+
clientInput,
|
|
949
|
+
serverInput,
|
|
950
|
+
)
|
|
951
|
+
const serverStream = new pairedMemoryStream(
|
|
952
|
+
streamCtx,
|
|
953
|
+
serverInput,
|
|
954
|
+
clientInput,
|
|
839
955
|
)
|
|
956
|
+
if (firstMsg != null) {
|
|
957
|
+
const err = serverInput.send(firstMsg)
|
|
958
|
+
if (err != null) {
|
|
959
|
+
return [null, err]
|
|
960
|
+
}
|
|
961
|
+
}
|
|
840
962
|
const pending = Promise.resolve(
|
|
841
|
-
this.invoker.InvokeMethod(service, method,
|
|
963
|
+
this.invoker.InvokeMethod(service, method, serverStream),
|
|
842
964
|
)
|
|
843
965
|
pending.then(([handled, err]) => {
|
|
844
966
|
if (!handled || err != null) {
|
|
845
|
-
|
|
967
|
+
clientStream.Close()
|
|
968
|
+
return
|
|
846
969
|
}
|
|
970
|
+
serverStream.CloseSend()
|
|
847
971
|
})
|
|
848
|
-
return [
|
|
972
|
+
return [clientStream, null]
|
|
849
973
|
}
|
|
850
974
|
}
|
|
851
975
|
|
|
@@ -936,7 +1060,9 @@ export class ServerRPC {
|
|
|
936
1060
|
stream,
|
|
937
1061
|
)
|
|
938
1062
|
const callErr = err ?? (handled ? null : ErrUnimplemented)
|
|
939
|
-
await this.writer?.WritePacket(
|
|
1063
|
+
await this.writer?.WritePacket(
|
|
1064
|
+
NewCallDataPacket(null, false, true, callErr),
|
|
1065
|
+
)
|
|
940
1066
|
return callErr
|
|
941
1067
|
}
|
|
942
1068
|
|
|
@@ -995,8 +1121,10 @@ export function NewServerPipe(server: Server | null): OpenStreamFunc {
|
|
|
995
1121
|
_ctx: context.Context,
|
|
996
1122
|
_msgHandler: PacketDataHandler,
|
|
997
1123
|
_closeHandler: CloseHandler,
|
|
998
|
-
): [PacketWriter | null, $.GoError] => [
|
|
999
|
-
|
|
1124
|
+
): [PacketWriter | null, $.GoError] => [
|
|
1125
|
+
new closedPacketWriter(),
|
|
1126
|
+
null,
|
|
1127
|
+
]) as OpenStreamFunc
|
|
1000
1128
|
if (server != null) {
|
|
1001
1129
|
openStream.__server = server
|
|
1002
1130
|
}
|