goscript 0.0.84 → 0.1.0

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 (188) hide show
  1. package/README.md +13 -1
  2. package/cmd/goscript/cmd_compile.go +70 -69
  3. package/cmd/goscript/cmd_compile_test.go +79 -0
  4. package/cmd/goscript/main.go +10 -5
  5. package/compiler/compile-request.go +218 -0
  6. package/compiler/compiler.go +16 -1336
  7. package/compiler/compliance_test.go +196 -0
  8. package/compiler/config.go +6 -13
  9. package/compiler/diagnostic.go +70 -0
  10. package/compiler/index.test.ts +28 -28
  11. package/compiler/index.ts +40 -72
  12. package/compiler/lowered-program.go +132 -0
  13. package/compiler/lowering.go +3576 -0
  14. package/compiler/override-registry.go +422 -0
  15. package/compiler/override-registry_test.go +207 -0
  16. package/compiler/package-graph.go +231 -0
  17. package/compiler/package-graph_test.go +281 -0
  18. package/compiler/result.go +13 -0
  19. package/compiler/runtime-contract.go +279 -0
  20. package/compiler/runtime-contract_test.go +90 -0
  21. package/compiler/semantic-model-types.go +110 -0
  22. package/compiler/semantic-model.go +922 -0
  23. package/compiler/semantic-model_test.go +416 -0
  24. package/compiler/service.go +133 -0
  25. package/compiler/skeleton_test.go +1145 -0
  26. package/compiler/typescript-emitter.go +663 -0
  27. package/compiler/wasm/compile.go +2 -3
  28. package/compiler/wasm/compile_test.go +29 -0
  29. package/compiler/wasm_api.go +10 -159
  30. package/dist/compiler/index.d.ts +1 -3
  31. package/dist/compiler/index.js +31 -55
  32. package/dist/compiler/index.js.map +1 -1
  33. package/dist/gs/builtin/builtin.d.ts +13 -0
  34. package/dist/gs/builtin/builtin.js +23 -0
  35. package/dist/gs/builtin/builtin.js.map +1 -1
  36. package/dist/gs/builtin/channel.d.ts +3 -3
  37. package/dist/gs/builtin/channel.js.map +1 -1
  38. package/dist/gs/builtin/hostio.d.ts +15 -1
  39. package/dist/gs/builtin/hostio.js +134 -49
  40. package/dist/gs/builtin/hostio.js.map +1 -1
  41. package/dist/gs/builtin/index.d.ts +1 -0
  42. package/dist/gs/builtin/index.js +1 -0
  43. package/dist/gs/builtin/index.js.map +1 -1
  44. package/dist/gs/builtin/slice.d.ts +1 -1
  45. package/dist/gs/builtin/slice.js.map +1 -1
  46. package/dist/gs/builtin/type.d.ts +11 -0
  47. package/dist/gs/builtin/type.js +55 -1
  48. package/dist/gs/builtin/type.js.map +1 -1
  49. package/dist/gs/bytes/buffer.gs.js.map +1 -1
  50. package/dist/gs/bytes/bytes.gs.js.map +1 -1
  51. package/dist/gs/bytes/reader.gs.js.map +1 -1
  52. package/dist/gs/context/context.js.map +1 -1
  53. package/dist/gs/crypto/rand/index.d.ts +5 -0
  54. package/dist/gs/crypto/rand/index.js +77 -0
  55. package/dist/gs/crypto/rand/index.js.map +1 -0
  56. package/dist/gs/encoding/json/index.d.ts +3 -0
  57. package/dist/gs/encoding/json/index.js +160 -0
  58. package/dist/gs/encoding/json/index.js.map +1 -0
  59. package/dist/gs/fmt/fmt.js.map +1 -1
  60. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.d.ts +1 -1
  61. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js +1 -1
  62. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js.map +1 -1
  63. package/dist/gs/github.com/aperturerobotics/wasivm/wazero/kernel/runtime/browser/browser.js.map +1 -1
  64. package/dist/gs/github.com/pkg/errors/errors.js.map +1 -1
  65. package/dist/gs/github.com/pkg/errors/stack.js.map +1 -1
  66. package/dist/gs/go/scanner/index.d.ts +29 -0
  67. package/dist/gs/go/scanner/index.js +120 -0
  68. package/dist/gs/go/scanner/index.js.map +1 -0
  69. package/dist/gs/go/token/index.d.ts +31 -0
  70. package/dist/gs/go/token/index.js +82 -0
  71. package/dist/gs/go/token/index.js.map +1 -0
  72. package/dist/gs/internal/abi/index.js.map +1 -1
  73. package/dist/gs/io/fs/fs.js.map +1 -1
  74. package/dist/gs/io/fs/readdir.js.map +1 -1
  75. package/dist/gs/io/fs/readfile.js.map +1 -1
  76. package/dist/gs/io/fs/stat.js.map +1 -1
  77. package/dist/gs/io/fs/sub.js.map +1 -1
  78. package/dist/gs/io/io.js.map +1 -1
  79. package/dist/gs/os/dir_unix.gs.js.map +1 -1
  80. package/dist/gs/os/error.gs.js +2 -4
  81. package/dist/gs/os/error.gs.js.map +1 -1
  82. package/dist/gs/os/exec.gs.js.map +1 -1
  83. package/dist/gs/os/exec_posix.gs.js.map +1 -1
  84. package/dist/gs/os/rawconn_js.gs.js.map +1 -1
  85. package/dist/gs/os/root_js.gs.js.map +1 -1
  86. package/dist/gs/os/tempfile.gs.js +66 -9
  87. package/dist/gs/os/tempfile.gs.js.map +1 -1
  88. package/dist/gs/os/types.gs.js.map +1 -1
  89. package/dist/gs/os/types_js.gs.js +9 -9
  90. package/dist/gs/os/types_js.gs.js.map +1 -1
  91. package/dist/gs/os/types_unix.gs.js.map +1 -1
  92. package/dist/gs/path/filepath/match.js.map +1 -1
  93. package/dist/gs/path/match.js.map +1 -1
  94. package/dist/gs/path/path.js.map +1 -1
  95. package/dist/gs/reflect/index.d.ts +2 -2
  96. package/dist/gs/reflect/index.js +1 -1
  97. package/dist/gs/reflect/index.js.map +1 -1
  98. package/dist/gs/reflect/map.js.map +1 -1
  99. package/dist/gs/reflect/type.d.ts +2 -1
  100. package/dist/gs/reflect/type.js +85 -14
  101. package/dist/gs/reflect/type.js.map +1 -1
  102. package/dist/gs/reflect/types.js.map +1 -1
  103. package/dist/gs/reflect/visiblefields.js.map +1 -1
  104. package/dist/gs/runtime/runtime.js.map +1 -1
  105. package/dist/gs/sort/sort.gs.js.map +1 -1
  106. package/dist/gs/strconv/atoi.gs.js.map +1 -1
  107. package/dist/gs/strconv/quote.gs.js.map +1 -1
  108. package/dist/gs/strings/builder.js.map +1 -1
  109. package/dist/gs/strings/reader.js.map +1 -1
  110. package/dist/gs/strings/replace.js.map +1 -1
  111. package/dist/gs/sync/atomic/type.gs.js.map +1 -1
  112. package/dist/gs/sync/atomic/value.gs.js.map +1 -1
  113. package/dist/gs/sync/sync.d.ts +1 -0
  114. package/dist/gs/sync/sync.js +12 -0
  115. package/dist/gs/sync/sync.js.map +1 -1
  116. package/dist/gs/time/time.js.map +1 -1
  117. package/dist/gs/unicode/unicode.js.map +1 -1
  118. package/go.mod +2 -2
  119. package/gs/builtin/builtin.ts +27 -0
  120. package/gs/builtin/hostio.test.ts +177 -0
  121. package/gs/builtin/hostio.ts +171 -56
  122. package/gs/builtin/index.ts +1 -0
  123. package/gs/builtin/runtime-contract.test.ts +230 -0
  124. package/gs/builtin/type.ts +84 -1
  125. package/gs/crypto/rand/index.test.ts +32 -0
  126. package/gs/crypto/rand/index.ts +90 -0
  127. package/gs/crypto/rand/meta.json +5 -0
  128. package/gs/encoding/json/index.test.ts +65 -0
  129. package/gs/encoding/json/index.ts +186 -0
  130. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.test.ts +23 -0
  131. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.ts +3 -1
  132. package/gs/github.com/aperturerobotics/wasivm/wazero/kernel/runtime/browser/meta.json +3 -1
  133. package/gs/go/scanner/index.test.ts +50 -0
  134. package/gs/go/scanner/index.ts +157 -0
  135. package/gs/go/token/index.test.ts +21 -0
  136. package/gs/go/token/index.ts +120 -0
  137. package/gs/os/file_unix_js.test.ts +50 -0
  138. package/gs/os/meta.json +1 -2
  139. package/gs/os/tempfile.gs.test.ts +85 -0
  140. package/gs/os/tempfile.gs.ts +71 -11
  141. package/gs/os/types_js.gs.ts +9 -9
  142. package/gs/reflect/index.ts +1 -1
  143. package/gs/reflect/type.ts +106 -17
  144. package/gs/reflect/typefor.test.ts +75 -0
  145. package/gs/sync/sync.test.ts +24 -0
  146. package/gs/sync/sync.ts +12 -0
  147. package/package.json +13 -13
  148. package/compiler/analysis.go +0 -3475
  149. package/compiler/analysis_test.go +0 -338
  150. package/compiler/assignment.go +0 -580
  151. package/compiler/builtin_test.go +0 -92
  152. package/compiler/code-writer.go +0 -115
  153. package/compiler/compiler_test.go +0 -149
  154. package/compiler/composite-lit.go +0 -779
  155. package/compiler/config_test.go +0 -62
  156. package/compiler/constraint.go +0 -86
  157. package/compiler/decl.go +0 -801
  158. package/compiler/expr-call-async.go +0 -188
  159. package/compiler/expr-call-builtins.go +0 -208
  160. package/compiler/expr-call-helpers.go +0 -382
  161. package/compiler/expr-call-make.go +0 -318
  162. package/compiler/expr-call-type-conversion.go +0 -520
  163. package/compiler/expr-call.go +0 -413
  164. package/compiler/expr-selector.go +0 -343
  165. package/compiler/expr-star.go +0 -82
  166. package/compiler/expr-type.go +0 -442
  167. package/compiler/expr-value.go +0 -89
  168. package/compiler/expr.go +0 -773
  169. package/compiler/field.go +0 -183
  170. package/compiler/gs_dependencies_test.go +0 -298
  171. package/compiler/lit.go +0 -322
  172. package/compiler/output.go +0 -72
  173. package/compiler/primitive.go +0 -149
  174. package/compiler/protobuf.go +0 -697
  175. package/compiler/sanitize.go +0 -100
  176. package/compiler/spec-struct.go +0 -995
  177. package/compiler/spec-value.go +0 -540
  178. package/compiler/spec.go +0 -725
  179. package/compiler/stmt-assign.go +0 -664
  180. package/compiler/stmt-for.go +0 -266
  181. package/compiler/stmt-range.go +0 -475
  182. package/compiler/stmt-select.go +0 -262
  183. package/compiler/stmt-type-switch.go +0 -147
  184. package/compiler/stmt.go +0 -1308
  185. package/compiler/type-assert.go +0 -386
  186. package/compiler/type-info.go +0 -156
  187. package/compiler/type-utils.go +0 -207
  188. package/compiler/type.go +0 -892
@@ -0,0 +1,230 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import {
4
+ byte,
5
+ callGenericMethod,
6
+ chanRecvWithOk,
7
+ functionValue,
8
+ genericZero,
9
+ int,
10
+ interfaceValue,
11
+ makeChannel,
12
+ makeMap,
13
+ mapGet,
14
+ mapSet,
15
+ markAsStructValue,
16
+ namedFunction,
17
+ newError,
18
+ pointerValue,
19
+ print,
20
+ println,
21
+ registerInterfaceType,
22
+ registerStructType,
23
+ resetHostRuntimeForTests,
24
+ TypeKind,
25
+ typeAssert,
26
+ typedNil,
27
+ unref,
28
+ varRef,
29
+ } from './index.js'
30
+
31
+ const originalDeno = (globalThis as any).Deno
32
+ const originalProcess = (globalThis as any).process
33
+
34
+ afterEach(() => {
35
+ vi.restoreAllMocks()
36
+
37
+ if (originalDeno === undefined) {
38
+ delete (globalThis as any).Deno
39
+ } else {
40
+ ;(globalThis as any).Deno = originalDeno
41
+ }
42
+
43
+ if (originalProcess === undefined) {
44
+ delete (globalThis as any).process
45
+ } else {
46
+ ;(globalThis as any).process = originalProcess
47
+ }
48
+
49
+ resetHostRuntimeForTests()
50
+ })
51
+
52
+ describe('builtin runtime contract helpers', () => {
53
+ it('writes print and println through the host runtime owner', () => {
54
+ const writes: Array<{ fd: number; text: string }> = []
55
+ const writeSync = vi.fn(
56
+ (
57
+ fd: number,
58
+ buffer: Uint8Array,
59
+ _offset?: number,
60
+ length?: number,
61
+ _position?: number | null,
62
+ ) => {
63
+ writes.push({
64
+ fd,
65
+ text: new TextDecoder().decode(
66
+ buffer.subarray(0, length ?? buffer.length),
67
+ ),
68
+ })
69
+ return length ?? buffer.length
70
+ },
71
+ )
72
+
73
+ delete (globalThis as any).Deno
74
+ ;(globalThis as any).process = {
75
+ getBuiltinModule: vi.fn(() => ({
76
+ readSync: vi.fn(),
77
+ writeSync,
78
+ })),
79
+ }
80
+ resetHostRuntimeForTests()
81
+
82
+ print('value:', 3)
83
+ println('done')
84
+
85
+ expect(writes).toEqual([
86
+ { fd: 1, text: 'value: 3' },
87
+ { fd: 1, text: 'done\n' },
88
+ ])
89
+ })
90
+
91
+ it('exposes numeric, varref, map, and error helpers', () => {
92
+ expect(int(1.9)).toBe(1)
93
+ expect(byte(257)).toBe(1)
94
+
95
+ const value = varRef(4)
96
+ value.value = 8
97
+ expect(unref(value)).toBe(8)
98
+ expect(pointerValue(value)).toBe(8)
99
+ expect(pointerValue({ ok: true })).toEqual({ ok: true })
100
+ expect(() => pointerValue(null)).toThrow('nil pointer dereference')
101
+
102
+ const m = makeMap<string, number>()
103
+ mapSet(m, 'answer', 42)
104
+ expect(mapGet(m, 'answer', 0)).toEqual([42, true])
105
+ expect(mapGet(m, 'missing', 0)).toEqual([0, false])
106
+
107
+ expect(newError('bad')?.Error()).toBe('bad')
108
+ })
109
+
110
+ it('exposes value and type descriptor helpers', () => {
111
+ class Runner {
112
+ public Run(): string {
113
+ return 'ok'
114
+ }
115
+ }
116
+
117
+ const runnerType = registerStructType(
118
+ 'phase5.Runner',
119
+ markAsStructValue(new Runner()),
120
+ [{ name: 'Run', args: [], returns: [{ type: 'string' }] }],
121
+ Runner,
122
+ )
123
+ const runnerInterface = registerInterfaceType(
124
+ 'phase5.RunnerInterface',
125
+ null,
126
+ [{ name: 'Run', args: [], returns: [{ type: 'string' }] }],
127
+ )
128
+
129
+ const value = new Runner()
130
+ expect(markAsStructValue(value)).toBe(value)
131
+ expect(typeAssert<Runner>(value, runnerType)).toEqual({
132
+ ok: true,
133
+ value,
134
+ })
135
+ expect(typeAssert<Runner>(new Runner(), runnerInterface).ok).toBe(true)
136
+ expect(typeAssert<Runner>(null, runnerInterface).ok).toBe(false)
137
+
138
+ const nil = typedNil('*main.Example')
139
+ expect(nil.__isTypedNil).toBe(true)
140
+ expect(nil.__goType).toBe('*main.Example')
141
+ expect(
142
+ typeAssert<Runner | null>(typedNil('*phase5.Runner'), {
143
+ kind: TypeKind.Pointer,
144
+ elemType: 'phase5.Runner',
145
+ }),
146
+ ).toEqual({ value: null, ok: true })
147
+ expect(TypeKind.Pointer).toBe('pointer')
148
+
149
+ class TypedDog {
150
+ public Name(this: TypedDog | null): string {
151
+ if (this === null) {
152
+ return 'unknown dog'
153
+ }
154
+ return 'dog'
155
+ }
156
+ }
157
+
158
+ registerStructType(
159
+ 'phase5.TypedDog',
160
+ new TypedDog(),
161
+ [{ name: 'Name', args: [], returns: [{ type: 'string' }] }],
162
+ TypedDog,
163
+ )
164
+ const dogInterface = registerInterfaceType(
165
+ 'phase5.DogInterface',
166
+ null,
167
+ [{ name: 'Name', args: [], returns: [{ type: 'string' }] }],
168
+ )
169
+ const nilDog = interfaceValue<{ Name(): string } | null>(
170
+ null,
171
+ '*phase5.TypedDog',
172
+ )
173
+ expect(nilDog).not.toBeNull()
174
+ expect(nilDog!.Name()).toBe('unknown dog')
175
+ expect(typeAssert<{ Name(): string }>(nilDog, dogInterface).ok).toBe(true)
176
+ expect(
177
+ typeAssert<TypedDog | null>(nilDog, {
178
+ kind: TypeKind.Pointer,
179
+ elemType: 'phase5.TypedDog',
180
+ }),
181
+ ).toEqual({ value: null, ok: true })
182
+
183
+ const greet = namedFunction((name: string) => `hello ${name}`, 'phase5.Greet')
184
+ expect(typeAssert<typeof greet>(greet, {
185
+ kind: TypeKind.Function,
186
+ name: 'phase5.Greet',
187
+ }).ok).toBe(true)
188
+ expect(
189
+ typeAssert<{ Name: string }>(
190
+ { Name: 'Alice' },
191
+ {
192
+ kind: TypeKind.Struct,
193
+ methods: [],
194
+ fields: {
195
+ Name: {
196
+ type: { kind: TypeKind.Basic, name: 'string' },
197
+ tag: 'json:"name"',
198
+ },
199
+ },
200
+ },
201
+ ).ok,
202
+ ).toBe(true)
203
+ const literal = functionValue((value: number) => String(value), {
204
+ kind: TypeKind.Function,
205
+ params: [{ kind: TypeKind.Basic, name: 'int' }],
206
+ results: [{ kind: TypeKind.Basic, name: 'string' }],
207
+ })
208
+ expect(literal(7)).toBe('7')
209
+ expect(literal).toHaveProperty('__typeInfo')
210
+
211
+ const genericArgs = {
212
+ T: {
213
+ zero: () => 0,
214
+ methods: {
215
+ String: (value: number) => String(value),
216
+ },
217
+ },
218
+ }
219
+ expect(genericZero(genericArgs, 'T', null)).toBe(0)
220
+ expect(callGenericMethod(genericArgs, 'T', 'String', 12)).toBe('12')
221
+ })
222
+
223
+ it('exposes channel helpers used by future lowering', async () => {
224
+ const channel = makeChannel<number>(1, 0, 'both')
225
+ await channel.send(7)
226
+ expect(await chanRecvWithOk(channel)).toEqual({ value: 7, ok: true })
227
+ channel.close()
228
+ expect(await chanRecvWithOk(channel)).toEqual({ value: 0, ok: false })
229
+ })
230
+ })
@@ -500,9 +500,12 @@ function matchesStructType(value: any, info: TypeInfo): boolean {
500
500
 
501
501
  if (fieldsExist && sameFieldCount && allFieldsInStruct) {
502
502
  return Object.entries(info.fields).every(([fieldName, fieldType]) => {
503
+ const fieldTypeInfo = isStructFieldInfo(fieldType)
504
+ ? fieldType.type
505
+ : fieldType
503
506
  return matchesType(
504
507
  value[fieldName],
505
- normalizeTypeInfo(fieldType as TypeInfo | string),
508
+ normalizeTypeInfo(fieldTypeInfo),
506
509
  )
507
510
  })
508
511
  }
@@ -1008,6 +1011,9 @@ export function typeAssert<T>(
1008
1011
 
1009
1012
  // Handle typed nil pointers (created by typedNil() for conversions like (*T)(nil))
1010
1013
  if (typeof value === 'object' && value !== null && value.__isTypedNil) {
1014
+ if (isInterfaceTypeInfo(normalizedType) && matchesInterfaceType(value, normalizedType)) {
1015
+ return { value: value as T, ok: true }
1016
+ }
1011
1017
  // For typed nils, we need to compare the stored type with the expected type
1012
1018
  if (isPointerTypeInfo(normalizedType)) {
1013
1019
  // Parse the stored type string and compare with expected type
@@ -1219,3 +1225,80 @@ export function typedNil(typeName: string): any {
1219
1225
  __isTypedNil: true,
1220
1226
  })
1221
1227
  }
1228
+
1229
+ export function interfaceValue<T>(value: unknown, typeName: string): T {
1230
+ if (value !== null && value !== undefined) {
1231
+ return value as T
1232
+ }
1233
+
1234
+ const nilValue = typedNil(typeName)
1235
+ if (!typeName.startsWith('*')) {
1236
+ return nilValue as T
1237
+ }
1238
+
1239
+ const dynamicType = typeRegistry.get(typeName.slice(1))
1240
+ if (!dynamicType || !isStructTypeInfo(dynamicType) || !dynamicType.ctor) {
1241
+ return nilValue as T
1242
+ }
1243
+
1244
+ const prototype = dynamicType.ctor.prototype as Record<string, unknown>
1245
+ for (const method of dynamicType.methods) {
1246
+ const implementation = prototype[method.name]
1247
+ if (typeof implementation !== 'function') {
1248
+ continue
1249
+ }
1250
+ Object.defineProperty(nilValue, method.name, {
1251
+ value: (...args: unknown[]) => implementation.call(null, ...args),
1252
+ enumerable: true,
1253
+ })
1254
+ }
1255
+ return nilValue as T
1256
+ }
1257
+
1258
+ export function namedFunction<T>(fn: T, typeName: string): T {
1259
+ if (typeof fn !== 'function') {
1260
+ return fn
1261
+ }
1262
+ return Object.assign(fn, { __goTypeName: typeName })
1263
+ }
1264
+
1265
+ export function functionValue<T extends (...args: any[]) => any>(
1266
+ fn: T,
1267
+ typeInfo: FunctionTypeInfo,
1268
+ ): T {
1269
+ return Object.assign(fn, { __typeInfo: typeInfo })
1270
+ }
1271
+
1272
+ export interface GenericTypeDescriptor<T = any> {
1273
+ type?: TypeInfo | string
1274
+ zero?: () => T
1275
+ methods?: Record<string, (receiver: T, ...args: any[]) => any>
1276
+ }
1277
+
1278
+ export type GenericTypeArgs = Record<string, GenericTypeDescriptor>
1279
+
1280
+ export function genericZero<T>(
1281
+ typeArgs: GenericTypeArgs | undefined,
1282
+ name: string,
1283
+ fallback: T,
1284
+ ): T {
1285
+ const zero = typeArgs?.[name]?.zero
1286
+ if (zero) {
1287
+ return zero() as T
1288
+ }
1289
+ return fallback
1290
+ }
1291
+
1292
+ export function callGenericMethod<T>(
1293
+ typeArgs: GenericTypeArgs | undefined,
1294
+ name: string,
1295
+ method: string,
1296
+ receiver: T,
1297
+ ...args: any[]
1298
+ ): any {
1299
+ const fn = typeArgs?.[name]?.methods?.[method]
1300
+ if (fn) {
1301
+ return fn(receiver, ...args)
1302
+ }
1303
+ return (receiver as any)[method](...args)
1304
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import * as $ from '@goscript/builtin/index.js'
4
+
5
+ import { Read, Reader, Text } from './index.js'
6
+
7
+ describe('crypto/rand override', () => {
8
+ it('fills byte slices from Web Crypto', () => {
9
+ const buf = new Uint8Array(32)
10
+ const [n, err] = Read(buf)
11
+
12
+ expect(err).toBeNull()
13
+ expect(n).toBe(32)
14
+ expect(buf.some((b) => b !== 0)).toBe(true)
15
+ })
16
+
17
+ it('exposes Reader as an io.Reader-compatible source', () => {
18
+ const buf = $.makeSlice<number>(12, 12, 'byte')
19
+ const [n, err] = Reader.Read(buf)
20
+
21
+ expect(err).toBeNull()
22
+ expect(n).toBe(12)
23
+ expect(Array.from(buf ?? []).some((b) => b !== 0)).toBe(true)
24
+ })
25
+
26
+ it('generates base32 text tokens', () => {
27
+ const token = Text()
28
+
29
+ expect(token).toHaveLength(26)
30
+ expect(token).toMatch(/^[A-Z2-7]+$/)
31
+ })
32
+ })
@@ -0,0 +1,90 @@
1
+ import * as $ from '@goscript/builtin/index.js'
2
+ import * as io from '@goscript/io/index.js'
3
+
4
+ const base32alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
5
+ const maxGetRandomValuesBytes = 65536
6
+
7
+ class RandError {
8
+ constructor(private readonly message: string) {}
9
+
10
+ Error(): string {
11
+ return this.message
12
+ }
13
+ }
14
+
15
+ class WebCryptoReader implements io.Reader {
16
+ Read(p: $.Bytes): [number, $.GoError] {
17
+ const err = fillSecureBytes(p)
18
+ if (err != null) {
19
+ return [0, err]
20
+ }
21
+ return [$.len(p), null]
22
+ }
23
+ }
24
+
25
+ export let Reader: io.Reader = new WebCryptoReader()
26
+
27
+ export function Read(b: $.Bytes): [number, $.GoError] {
28
+ const [n, err] = Reader.Read(b)
29
+ if (err != null) {
30
+ return [n, err]
31
+ }
32
+ if (n !== $.len(b)) {
33
+ return [n, io.ErrUnexpectedEOF]
34
+ }
35
+ return [n, null]
36
+ }
37
+
38
+ export function Text(): string {
39
+ const src = new Uint8Array(26)
40
+ const [, err] = Read(src)
41
+ if (err != null) {
42
+ throw new Error(err.Error())
43
+ }
44
+
45
+ let out = ''
46
+ for (const b of src) {
47
+ out += base32alphabet[b % 32]
48
+ }
49
+ return out
50
+ }
51
+
52
+ function fillSecureBytes(dst: $.Bytes): $.GoError {
53
+ const length = $.len(dst)
54
+ if (length === 0) {
55
+ return null
56
+ }
57
+
58
+ const crypto = secureCrypto()
59
+ if (crypto == null) {
60
+ return new RandError('crypto/rand: Web Crypto getRandomValues is unavailable')
61
+ }
62
+
63
+ if (dst instanceof Uint8Array) {
64
+ fillUint8Array(crypto, dst)
65
+ return null
66
+ }
67
+
68
+ const tmp = new Uint8Array(length)
69
+ fillUint8Array(crypto, tmp)
70
+ $.copy(dst, tmp)
71
+ return null
72
+ }
73
+
74
+ function fillUint8Array(crypto: Crypto, dst: Uint8Array): void {
75
+ for (let offset = 0; offset < dst.length; offset += maxGetRandomValuesBytes) {
76
+ const chunk = dst.subarray(
77
+ offset,
78
+ Math.min(offset + maxGetRandomValuesBytes, dst.length),
79
+ ) as Uint8Array<ArrayBuffer>
80
+ crypto.getRandomValues(chunk)
81
+ }
82
+ }
83
+
84
+ function secureCrypto(): Crypto | null {
85
+ const crypto = globalThis.crypto
86
+ if (crypto && typeof crypto.getRandomValues === 'function') {
87
+ return crypto
88
+ }
89
+ return null
90
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": [
3
+ "io"
4
+ ]
5
+ }
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import * as $ from '@goscript/builtin/index.js'
4
+
5
+ import { Marshal, Unmarshal } from './index.js'
6
+
7
+ class Person {
8
+ public _fields = {
9
+ Name: $.varRef(''),
10
+ Age: $.varRef(0),
11
+ Active: $.varRef(false),
12
+ }
13
+
14
+ static __typeInfo = $.registerStructType(
15
+ 'test.Person',
16
+ new Person(),
17
+ [],
18
+ Person,
19
+ {
20
+ Name: { type: { kind: $.TypeKind.Basic, name: 'string' }, tag: 'json:"name"' },
21
+ Age: { type: { kind: $.TypeKind.Basic, name: 'int' }, tag: 'json:"age"' },
22
+ Active: { type: { kind: $.TypeKind.Basic, name: 'bool' }, tag: 'json:"active"' },
23
+ },
24
+ )
25
+ }
26
+
27
+ describe('encoding/json override', () => {
28
+ it('marshals struct fields through json tags', () => {
29
+ const person = new Person()
30
+ person._fields.Name.value = 'Alice'
31
+ person._fields.Age.value = 30
32
+ person._fields.Active.value = true
33
+
34
+ const [data, err] = Marshal(person)
35
+
36
+ expect(err).toBeNull()
37
+ expect($.bytesToString(data)).toBe(
38
+ '{"name":"Alice","age":30,"active":true}',
39
+ )
40
+ })
41
+
42
+ it('unmarshals into struct and map pointers', () => {
43
+ const person = $.varRef(new Person())
44
+ const personErr = Unmarshal(
45
+ $.stringToBytes('{"name":"Bob","age":25,"active":false}'),
46
+ person,
47
+ )
48
+
49
+ expect(personErr).toBeNull()
50
+ expect(person.value._fields.Name.value).toBe('Bob')
51
+ expect(person.value._fields.Age.value).toBe(25)
52
+ expect(person.value._fields.Active.value).toBe(false)
53
+
54
+ const mapRef: $.VarRef<Map<string, unknown> | null> = $.varRef(null)
55
+ const mapErr = Unmarshal(
56
+ $.stringToBytes('{"name":"Carol","age":22,"active":true}'),
57
+ mapRef,
58
+ )
59
+
60
+ expect(mapErr).toBeNull()
61
+ expect(mapRef.value?.get('name')).toBe('Carol')
62
+ expect(mapRef.value?.get('age')).toBe(22)
63
+ expect(mapRef.value?.get('active')).toBe(true)
64
+ })
65
+ })
@@ -0,0 +1,186 @@
1
+ import * as $ from '@goscript/builtin/index.js'
2
+
3
+ export function Marshal(v: unknown): [$.Slice<number>, $.GoError] {
4
+ try {
5
+ return [$.stringToBytes(JSON.stringify(marshalValue(v))), null]
6
+ } catch (err) {
7
+ return [null, goError(err)]
8
+ }
9
+ }
10
+
11
+ export function Unmarshal(data: $.Slice<number>, v: unknown): $.GoError {
12
+ try {
13
+ assignDecodedValue(v, JSON.parse($.bytesToString(data)))
14
+ return null
15
+ } catch (err) {
16
+ return goError(err)
17
+ }
18
+ }
19
+
20
+ function marshalValue(v: unknown): unknown {
21
+ if ($.isVarRef(v)) {
22
+ return marshalValue(v.value)
23
+ }
24
+ if (v === null || v === undefined) {
25
+ return null
26
+ }
27
+ if (typeof v !== 'object') {
28
+ return v
29
+ }
30
+ if (v instanceof Uint8Array) {
31
+ return Array.from(v).map(marshalValue)
32
+ }
33
+ if (Array.isArray(v)) {
34
+ return v.map(marshalValue)
35
+ }
36
+ if (v instanceof Map) {
37
+ const out: Record<string, unknown> = {}
38
+ for (const [key, value] of v.entries()) {
39
+ out[String(key)] = marshalValue(value)
40
+ }
41
+ return out
42
+ }
43
+ if (!isStructValue(v)) {
44
+ return v
45
+ }
46
+
47
+ const out: Record<string, unknown> = {}
48
+ const typeFields = structFieldMetadata(v)
49
+ for (const [fieldName, ref] of Object.entries(v._fields)) {
50
+ const jsonName = jsonFieldName(fieldName, typeFields[fieldName]?.tag)
51
+ if (jsonName === '') {
52
+ continue
53
+ }
54
+ out[jsonName] = marshalValue(ref.value)
55
+ }
56
+ return out
57
+ }
58
+
59
+ function assignDecodedValue(target: unknown, decoded: unknown): void {
60
+ if ($.isVarRef(target)) {
61
+ if (isStructValue(target.value) && isPlainObject(decoded)) {
62
+ assignStructFields(target.value, decoded)
63
+ return
64
+ }
65
+ if (isPlainObject(decoded)) {
66
+ target.value = objectToMap(decoded)
67
+ return
68
+ }
69
+ target.value = decoded
70
+ return
71
+ }
72
+ if (isStructValue(target) && isPlainObject(decoded)) {
73
+ assignStructFields(target, decoded)
74
+ }
75
+ }
76
+
77
+ function assignStructFields(
78
+ target: { _fields: Record<string, $.VarRef<unknown>> },
79
+ decoded: Record<string, unknown>,
80
+ ): void {
81
+ const typeFields = structFieldMetadata(target)
82
+ for (const [fieldName, ref] of Object.entries(target._fields)) {
83
+ const jsonName = jsonFieldName(fieldName, typeFields[fieldName]?.tag)
84
+ if (
85
+ jsonName !== '' &&
86
+ Object.prototype.hasOwnProperty.call(decoded, jsonName)
87
+ ) {
88
+ ref.value = decoded[jsonName]
89
+ }
90
+ }
91
+ }
92
+
93
+ function objectToMap(decoded: Record<string, unknown>): Map<string, unknown> {
94
+ const out = new Map<string, unknown>()
95
+ for (const [key, value] of Object.entries(decoded)) {
96
+ out.set(key, isPlainObject(value) ? objectToMap(value) : value)
97
+ }
98
+ return out
99
+ }
100
+
101
+ function isStructValue(
102
+ value: unknown,
103
+ ): value is { _fields: Record<string, $.VarRef<unknown>> } {
104
+ if (value === null || typeof value !== 'object') {
105
+ return false
106
+ }
107
+ const fields = Reflect.get(value, '_fields')
108
+ return (
109
+ fields !== null &&
110
+ fields !== undefined &&
111
+ typeof fields === 'object' &&
112
+ !Array.isArray(fields)
113
+ )
114
+ }
115
+
116
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
117
+ return (
118
+ value !== null &&
119
+ typeof value === 'object' &&
120
+ !Array.isArray(value) &&
121
+ !(value instanceof Uint8Array) &&
122
+ !(value instanceof Map)
123
+ )
124
+ }
125
+
126
+ function structFieldMetadata(
127
+ value: unknown,
128
+ ): Record<string, { tag?: string }> {
129
+ if (value === null || typeof value !== 'object') {
130
+ return {}
131
+ }
132
+ const ctor = Reflect.get(value, 'constructor')
133
+ if (
134
+ ctor === null ||
135
+ ctor === undefined ||
136
+ (typeof ctor !== 'object' && typeof ctor !== 'function')
137
+ ) {
138
+ return {}
139
+ }
140
+ const typeInfo = Reflect.get(ctor, '__typeInfo')
141
+ if (
142
+ typeInfo === null ||
143
+ typeInfo === undefined ||
144
+ typeof typeInfo !== 'object'
145
+ ) {
146
+ return {}
147
+ }
148
+ const fields = Reflect.get(typeInfo, 'fields')
149
+ if (isPlainObject(fields)) {
150
+ const out: Record<string, { tag?: string }> = {}
151
+ for (const [name, field] of Object.entries(fields)) {
152
+ if (!isPlainObject(field)) {
153
+ continue
154
+ }
155
+ const tag = field.tag
156
+ out[name] = typeof tag === 'string' ? { tag } : {}
157
+ }
158
+ return out
159
+ }
160
+ return {}
161
+ }
162
+
163
+ function goError(err: unknown): $.GoError {
164
+ if (err instanceof Error) {
165
+ return $.toGoError(err)
166
+ }
167
+ return $.newError(String(err))
168
+ }
169
+
170
+ function jsonFieldName(fieldName: string, tag: string | undefined): string {
171
+ if (tag === undefined || !tag.startsWith('json:"')) {
172
+ return fieldName
173
+ }
174
+ const end = tag.indexOf('"', 'json:"'.length)
175
+ if (end < 0) {
176
+ return fieldName
177
+ }
178
+ const name = tag.slice('json:"'.length, end).split(',')[0]
179
+ if (name === '-') {
180
+ return ''
181
+ }
182
+ if (name === '') {
183
+ return fieldName
184
+ }
185
+ return name
186
+ }