@algosail/lang 0.2.12 → 0.5.1

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 (145) hide show
  1. package/bin/sail.mjs +4 -0
  2. package/cli/run-cli.js +176 -0
  3. package/index.js +12 -2
  4. package/lib/codegen/README.md +230 -0
  5. package/lib/codegen/codegen-diagnostics.js +164 -0
  6. package/lib/codegen/compile-graph.js +107 -0
  7. package/lib/codegen/emit-adt.js +177 -0
  8. package/lib/codegen/emit-body.js +1265 -0
  9. package/lib/codegen/emit-builtin.js +371 -0
  10. package/lib/codegen/emit-jsdoc-sail.js +383 -0
  11. package/lib/codegen/emit-module.js +498 -0
  12. package/lib/codegen/esm-imports.js +26 -0
  13. package/lib/codegen/index.js +69 -0
  14. package/lib/codegen/out-layout.js +102 -0
  15. package/lib/ffi/extract-jsdoc-sail.js +34 -0
  16. package/lib/io-node/index.js +4 -0
  17. package/lib/io-node/package-root.js +18 -0
  18. package/lib/io-node/read-file.js +12 -0
  19. package/lib/io-node/resolve-package.js +24 -0
  20. package/lib/io-node/resolve-sail-names-from-disk.js +21 -0
  21. package/lib/ir/assert-json-serializable.js +30 -0
  22. package/lib/ir/attach-call-effects.js +108 -0
  23. package/lib/ir/bind-values.js +594 -0
  24. package/lib/ir/build-module-ir.js +290 -0
  25. package/lib/ir/index.js +31 -0
  26. package/lib/ir/lower-body-steps.js +170 -0
  27. package/lib/ir/module-metadata.js +65 -0
  28. package/lib/ir/schema-version.js +15 -0
  29. package/lib/ir/serialize.js +202 -0
  30. package/lib/ir/stitch-types.js +92 -0
  31. package/lib/names/adt-autogen.js +22 -0
  32. package/lib/names/import-path.js +28 -0
  33. package/lib/names/index.js +1 -0
  34. package/lib/names/local-declarations.js +127 -0
  35. package/lib/names/lower-first.js +6 -0
  36. package/lib/names/module-scope.js +120 -0
  37. package/lib/names/resolve-sail.js +365 -0
  38. package/lib/names/walk-ast-refs.js +91 -0
  39. package/lib/parse/ast-build.js +51 -0
  40. package/lib/parse/ast-spec.js +212 -0
  41. package/lib/parse/builtins-set.js +12 -0
  42. package/lib/parse/diagnostics.js +180 -0
  43. package/lib/parse/index.js +46 -0
  44. package/lib/parse/lexer.js +390 -0
  45. package/lib/parse/parse-source.js +912 -0
  46. package/lib/typecheck/adt-autogen-sigs.js +345 -0
  47. package/lib/typecheck/build-type-env.js +148 -0
  48. package/lib/typecheck/builtin-signatures.js +183 -0
  49. package/lib/typecheck/check-word-body.js +1021 -0
  50. package/lib/typecheck/effect-decl.js +124 -0
  51. package/lib/typecheck/index.js +55 -0
  52. package/lib/typecheck/normalize-sig.js +369 -0
  53. package/lib/typecheck/stack-step-snapshots.js +56 -0
  54. package/lib/typecheck/unify-type.js +665 -0
  55. package/lib/typecheck/validate-adt.js +201 -0
  56. package/package.json +4 -9
  57. package/scripts/regen-demo-full-syntax-ast.mjs +22 -0
  58. package/test/cli/sail-cli.test.js +64 -0
  59. package/test/codegen/compile-bracket-ffi-e2e.test.js +64 -0
  60. package/test/codegen/compile-stage0.test.js +128 -0
  61. package/test/codegen/compile-stage4-layout.test.js +124 -0
  62. package/test/codegen/e2e-prelude-ffi-adt/app/extra.sail +6 -0
  63. package/test/codegen/e2e-prelude-ffi-adt/app/lib.sail +34 -0
  64. package/test/codegen/e2e-prelude-ffi-adt/app/main.sail +28 -0
  65. package/test/codegen/e2e-prelude-ffi-adt/artifacts/.gitignore +2 -0
  66. package/test/codegen/e2e-prelude-ffi-adt/ffi/helpers.js +27 -0
  67. package/test/codegen/e2e-prelude-ffi-adt.test.js +100 -0
  68. package/test/codegen/emit-adt-stage6.test.js +168 -0
  69. package/test/codegen/emit-async-stage5.test.js +164 -0
  70. package/test/codegen/emit-body-stage2.test.js +139 -0
  71. package/test/codegen/emit-body.test.js +163 -0
  72. package/test/codegen/emit-builtins-stage7.test.js +258 -0
  73. package/test/codegen/emit-diagnostics-stage9.test.js +90 -0
  74. package/test/codegen/emit-jsdoc-stage8.test.js +113 -0
  75. package/test/codegen/emit-module-stage3.test.js +78 -0
  76. package/test/conformance/conformance-ir-l4.test.js +38 -0
  77. package/test/conformance/conformance-l5-codegen.test.js +111 -0
  78. package/test/conformance/conformance-runner.js +91 -0
  79. package/test/conformance/conformance-suite-l3.test.js +32 -0
  80. package/test/ffi/prelude-jsdoc.test.js +49 -0
  81. package/test/fixtures/demo-full-syntax.ast.json +1471 -0
  82. package/test/fixtures/demo-full-syntax.sail +35 -0
  83. package/test/fixtures/io-node-ffi-adt/ffi.js +7 -0
  84. package/test/fixtures/io-node-ffi-adt/use.sail +4 -0
  85. package/test/fixtures/io-node-mini/dep.sail +2 -0
  86. package/test/fixtures/io-node-mini/entry.sail +4 -0
  87. package/test/fixtures/io-node-prelude/entry.sail +4 -0
  88. package/test/fixtures/io-node-reexport-chain/a.sail +4 -0
  89. package/test/fixtures/io-node-reexport-chain/b.sail +2 -0
  90. package/test/fixtures/io-node-reexport-chain/c.sail +2 -0
  91. package/test/io-node/resolve-disk.test.js +59 -0
  92. package/test/ir/bind-values.test.js +84 -0
  93. package/test/ir/build-module-ir.test.js +100 -0
  94. package/test/ir/call-effects.test.js +97 -0
  95. package/test/ir/ffi-bracket-ir.test.js +59 -0
  96. package/test/ir/full-ir-document.test.js +51 -0
  97. package/test/ir/ir-document-assert.js +67 -0
  98. package/test/ir/lower-body-steps.test.js +90 -0
  99. package/test/ir/module-metadata.test.js +42 -0
  100. package/test/ir/serialization-model.test.js +172 -0
  101. package/test/ir/stitch-types.test.js +74 -0
  102. package/test/names/l2-resolve-adt-autogen.test.js +155 -0
  103. package/test/names/l2-resolve-bracket-ffi.test.js +108 -0
  104. package/test/names/l2-resolve-declaration-and-bracket-errors.test.js +276 -0
  105. package/test/names/l2-resolve-graph.test.js +105 -0
  106. package/test/names/l2-resolve-single-file.test.js +79 -0
  107. package/test/parse/ast-spec.test.js +56 -0
  108. package/test/parse/ast.test.js +476 -0
  109. package/test/parse/contract.test.js +37 -0
  110. package/test/parse/fixtures-full-syntax.test.js +24 -0
  111. package/test/parse/helpers.js +27 -0
  112. package/test/parse/l0-lex-diagnostics-matrix.test.js +59 -0
  113. package/test/parse/l0-lex.test.js +40 -0
  114. package/test/parse/l1-diagnostics.test.js +77 -0
  115. package/test/parse/l1-import.test.js +28 -0
  116. package/test/parse/l1-parse-diagnostics-matrix.test.js +32 -0
  117. package/test/parse/l1-top-level.test.js +47 -0
  118. package/test/parse/l1-types.test.js +31 -0
  119. package/test/parse/l1-words.test.js +49 -0
  120. package/test/parse/l2-diagnostics-contract.test.js +67 -0
  121. package/test/parse/l3-diagnostics-contract.test.js +66 -0
  122. package/test/typecheck/adt-decl-stage2.test.js +83 -0
  123. package/test/typecheck/container-contract-e1309.test.js +258 -0
  124. package/test/typecheck/ffi-bracket-l3.test.js +61 -0
  125. package/test/typecheck/l3-diagnostics-matrix.test.js +248 -0
  126. package/test/typecheck/l3-partial-pipeline.test.js +74 -0
  127. package/test/typecheck/opaque-ffi-type.test.js +78 -0
  128. package/test/typecheck/sig-type-stage3.test.js +190 -0
  129. package/test/typecheck/stack-check-stage4.test.js +149 -0
  130. package/test/typecheck/stack-check-stage5.test.js +74 -0
  131. package/test/typecheck/stack-check-stage6.test.js +56 -0
  132. package/test/typecheck/stack-check-stage7.test.js +160 -0
  133. package/test/typecheck/stack-check-stage8.test.js +146 -0
  134. package/test/typecheck/stack-check-stage9.test.js +105 -0
  135. package/test/typecheck/typecheck-env.test.js +53 -0
  136. package/test/typecheck/typecheck-pipeline.test.js +37 -0
  137. package/README.md +0 -37
  138. package/cli/sail.js +0 -151
  139. package/cli/typecheck.js +0 -39
  140. package/docs/ARCHITECTURE.md +0 -50
  141. package/docs/CHANGELOG.md +0 -18
  142. package/docs/FFI-GUIDE.md +0 -65
  143. package/docs/RELEASE.md +0 -36
  144. package/docs/TESTING.md +0 -86
  145. package/test/integration.test.js +0 -61
@@ -0,0 +1,383 @@
1
+ /**
2
+ * L5 этап 8: JSDoc с `@sail` в сгенерированном ESM (RFC-compile-0.1 §11, RFC-0.1 §10.1).
3
+ * Тот же контрактный формат, что у ручного FFI: произвольный префикс до `@sail`, затем только Sail.
4
+ */
5
+ import path from 'node:path'
6
+
7
+ /**
8
+ * @param {string} line
9
+ * @returns {string}
10
+ */
11
+ function escapeJSDocLine (line) {
12
+ return line.replace(/\*\//g, '*\\/')
13
+ }
14
+
15
+ /**
16
+ * @param {string[]} docEntries inner text of each `-- … --` block
17
+ * @returns {string} Sail `doc_block` fragments
18
+ */
19
+ export function formatDocBlocksAsSail (docEntries) {
20
+ if (!Array.isArray(docEntries) || docEntries.length === 0) return ''
21
+ return docEntries.map((t) => `-- ${String(t)} --`).join(' ')
22
+ }
23
+
24
+ /**
25
+ * @param {string[]} lines prose without leading `*`
26
+ * @returns {string[]}
27
+ */
28
+ function proseToStarLines (lines) {
29
+ const out = []
30
+ for (const raw of lines) {
31
+ for (const part of String(raw).split('\n')) {
32
+ out.push(escapeJSDocLine(part))
33
+ }
34
+ }
35
+ return out
36
+ }
37
+
38
+ /**
39
+ * @param {{ proseLines?: string[], sailText: string }} opts
40
+ * @returns {string}
41
+ */
42
+ export function formatJSDocSailBlock ({ proseLines = [], sailText }) {
43
+ const sail = String(sailText).trim()
44
+ const body = []
45
+ const prose = proseToStarLines(proseLines)
46
+ for (const pl of prose) {
47
+ body.push(` * ${pl}`)
48
+ }
49
+ if (prose.length > 0) body.push(' *')
50
+ body.push(' * @sail')
51
+ for (const line of sail.split('\n')) {
52
+ body.push(` * ${line}`)
53
+ }
54
+ return `/**\n${body.join('\n')}\n */`
55
+ }
56
+
57
+ /**
58
+ * @param {object | null | undefined} expr
59
+ * @returns {string}
60
+ */
61
+ export function formatAstTypeExpr (expr) {
62
+ if (!expr || typeof expr !== 'object') return '_'
63
+ switch (expr.kind) {
64
+ case 'type_name':
65
+ return String(expr.name)
66
+ case 'type_var':
67
+ return String(expr.name)
68
+ case 'module_type_ref':
69
+ return `~${expr.module}/${expr.type}`
70
+ case 'paren_type':
71
+ return `(${formatAstTypeExpr(expr.inner)})`
72
+ case 'type_app': {
73
+ const ctor = String(expr.ctor)
74
+ const args = (expr.args ?? []).map(formatAstTypeExpr)
75
+ return `${ctor} ${args.join(' ')}`.trim()
76
+ }
77
+ case 'quotation_type':
78
+ return `Quote (${formatAstSignatureText(expr.inner)})`
79
+ default:
80
+ return '_'
81
+ }
82
+ }
83
+
84
+ /**
85
+ * @param {object | null | undefined} item
86
+ * @returns {string}
87
+ */
88
+ export function formatAstSigStackItem (item) {
89
+ if (!item || typeof item !== 'object') return '_'
90
+ switch (item.kind) {
91
+ case 'stack_var':
92
+ return `~${item.name}`
93
+ case 'sig_type_expr':
94
+ return formatAstTypeExpr(item.type)
95
+ case 'quotation_sig':
96
+ return `(${formatAstSignatureText(item.inner)})`
97
+ case 'named_quotation_sig': {
98
+ const inner = formatAstSignatureText(item.quotation.inner)
99
+ return `${item.prefix}: (${inner})`
100
+ }
101
+ default:
102
+ return '_'
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @param {{ offset: number }} a
108
+ * @param {{ offset: number }} b
109
+ * @returns {number}
110
+ */
111
+ function cmpSpan (a, b) {
112
+ return (a?.offset ?? 0) - (b?.offset ?? 0)
113
+ }
114
+
115
+ /**
116
+ * @param {object} sig AstSignature
117
+ * @returns {string}
118
+ */
119
+ export function formatAstSignatureText (sig) {
120
+ if (!sig || typeof sig !== 'object') return '->'
121
+ const left = (sig.left ?? []).map(formatAstSigStackItem).join(' ')
122
+ /** @type {{ span: object, emit: () => string }[]} */
123
+ const rightParts = []
124
+ for (const it of sig.right ?? []) {
125
+ const sp = it.span?.start ?? { offset: 0 }
126
+ rightParts.push({ span: sp, emit: () => formatAstSigStackItem(it) })
127
+ }
128
+ for (const e of sig.effectsAdd ?? []) {
129
+ if (e.side !== 'right') continue
130
+ const sp = e.span?.start ?? { offset: 0 }
131
+ rightParts.push({ span: sp, emit: () => `+${e.name}` })
132
+ }
133
+ for (const e of sig.effectsRemove ?? []) {
134
+ if (e.side !== 'right') continue
135
+ const sp = e.span?.start ?? { offset: 0 }
136
+ rightParts.push({ span: sp, emit: () => `-${e.name}` })
137
+ }
138
+ rightParts.sort((a, b) => cmpSpan(a.span, b.span))
139
+ const right = rightParts.map((p) => p.emit()).join(' ')
140
+ if (left.length === 0 && right.length === 0) return '->'
141
+ if (left.length === 0) return `-> ${right}`.trim()
142
+ if (right.length === 0) return `${left} ->`.trim()
143
+ return `${left} -> ${right}`
144
+ }
145
+
146
+ /**
147
+ * @param {object | null | undefined} slot normalized / wire slot
148
+ * @returns {string}
149
+ */
150
+ export function formatWireTypeSlot (slot) {
151
+ if (!slot || typeof slot !== 'object') return '_'
152
+ switch (slot.kind) {
153
+ case 'prim':
154
+ case 'opaque':
155
+ return String(slot.name)
156
+ case 'tvar':
157
+ return String(slot.name)
158
+ case 'stack_label':
159
+ return `~${slot.name}`
160
+ case 'mod_adt':
161
+ return `~${slot.module}/${slot.type}`
162
+ case 'adt':
163
+ return String(slot.type)
164
+ case 'app': {
165
+ const args = (slot.args ?? []).map(formatWireTypeSlot)
166
+ return `${slot.ctor} ${args.join(' ')}`.trim()
167
+ }
168
+ case 'quote':
169
+ return `(${formatWireSignatureText(slot.inner)})`
170
+ case 'named_quote': {
171
+ const inner = formatWireSignatureText(slot.inner)
172
+ return `${slot.prefix}: (${inner})`
173
+ }
174
+ default:
175
+ return '_'
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @param {object | null | undefined} sig wire normalizedSig
181
+ * @returns {string}
182
+ */
183
+ export function formatWireSignatureText (sig) {
184
+ if (!sig || typeof sig !== 'object') return '->'
185
+ const left = (sig.left ?? []).map(formatWireTypeSlot).join(' ')
186
+ const right = (sig.right ?? []).map(formatWireTypeSlot).join(' ')
187
+ const adds = (sig.effectsAdd ?? [])
188
+ .filter((e) => e.side !== 'left')
189
+ .map((e) => `+${e.name}`)
190
+ const subs = (sig.effectsRemove ?? [])
191
+ .filter((e) => e.side !== 'left')
192
+ .map((e) => `-${e.name}`)
193
+ const rightAll = [right, ...adds, ...subs].filter(Boolean).join(' ')
194
+ if (left.length === 0 && rightAll.length === 0) return '->'
195
+ if (left.length === 0) return `-> ${rightAll}`.trim()
196
+ if (rightAll.length === 0) return `${left} ->`.trim()
197
+ return `${left} -> ${rightAll}`
198
+ }
199
+
200
+ /**
201
+ * @param {object} item sum_type AST
202
+ * @returns {string}
203
+ */
204
+ export function formatSumTypeDeclSail (item) {
205
+ const params =
206
+ Array.isArray(item.typeParams) && item.typeParams.length > 0
207
+ ? ` ${item.typeParams.join(' ')}`
208
+ : ''
209
+ const head = `& ${item.name}${params} ${formatDocBlocksAsSail(item.doc ?? [])}`.trimEnd()
210
+ const lines = [head]
211
+ for (const tag of item.tags ?? []) {
212
+ const pl = tag.payload ? ` ${formatAstTypeExpr(tag.payload)}` : ''
213
+ const piece =
214
+ `| ${tag.name}${pl} ${formatDocBlocksAsSail(tag.doc ?? [])}`.trimEnd()
215
+ lines.push(piece)
216
+ }
217
+ return lines.join('\n')
218
+ }
219
+
220
+ /**
221
+ * @param {object} item product_type AST
222
+ * @returns {string}
223
+ */
224
+ export function formatProductTypeDeclSail (item) {
225
+ const params =
226
+ Array.isArray(item.typeParams) && item.typeParams.length > 0
227
+ ? ` ${item.typeParams.join(' ')}`
228
+ : ''
229
+ const head = `& ${item.name}${params} ${formatDocBlocksAsSail(item.doc ?? [])}`.trimEnd()
230
+ const lines = [head]
231
+ for (const f of item.fields ?? []) {
232
+ const piece =
233
+ `: ${f.name} ${formatAstTypeExpr(f.type)} ${formatDocBlocksAsSail(f.doc ?? [])}`.trimEnd()
234
+ lines.push(piece)
235
+ }
236
+ return lines.join('\n')
237
+ }
238
+
239
+ /**
240
+ * Собирает строки «человеческой» документации до `@sail` (JSDoc префикс).
241
+ *
242
+ * @param {string[]} doc
243
+ * @returns {string[]}
244
+ */
245
+ function proseFromDocEntries (doc) {
246
+ if (!Array.isArray(doc) || doc.length === 0) return []
247
+ return doc.flatMap((d) => String(d).split('\n'))
248
+ }
249
+
250
+ /**
251
+ * @param {string} modulePath
252
+ * @param {import('../typecheck/build-type-env.js').TypecheckEnv} env
253
+ * @returns {{ ok: true, block: string } | { ok: false, message: string }}
254
+ */
255
+ export function formatModuleSailHeaderBlock (modulePath, env) {
256
+ const normPath = path.normalize(modulePath)
257
+ const snap = env.snapshots?.get(normPath)
258
+ const scope = env.scopeByPath?.get(normPath)
259
+ if (!snap?.ok || !Array.isArray(snap.items) || !scope?.exportTypes) {
260
+ return { ok: true, block: '' }
261
+ }
262
+
263
+ /** @type {string[]} */
264
+ const proseLines = []
265
+ /** @type {string[]} */
266
+ const sailChunks = []
267
+
268
+ for (const it of snap.items) {
269
+ if (it.kind === 'sum_type' && scope.exportTypes.has(it.name)) {
270
+ proseLines.push(...proseFromDocEntries(it.doc))
271
+ try {
272
+ sailChunks.push(formatSumTypeDeclSail(it))
273
+ } catch (e) {
274
+ const msg = e instanceof Error ? e.message : String(e)
275
+ return { ok: false, message: `sum &${it.name}: ${msg}` }
276
+ }
277
+ } else if (it.kind === 'product_type' && scope.exportTypes.has(it.name)) {
278
+ proseLines.push(...proseFromDocEntries(it.doc))
279
+ try {
280
+ sailChunks.push(formatProductTypeDeclSail(it))
281
+ } catch (e) {
282
+ const msg = e instanceof Error ? e.message : String(e)
283
+ return { ok: false, message: `product &${it.name}: ${msg}` }
284
+ }
285
+ }
286
+ }
287
+
288
+ if (sailChunks.length === 0) {
289
+ return { ok: true, block: '' }
290
+ }
291
+
292
+ const sailText = sailChunks.join('\n\n')
293
+ try {
294
+ const block = formatJSDocSailBlock({
295
+ proseLines,
296
+ sailText
297
+ })
298
+ return { ok: true, block }
299
+ } catch (e) {
300
+ const msg = e instanceof Error ? e.message : String(e)
301
+ return { ok: false, message: msg }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * @param {object | null | undefined} snap
307
+ * @param {string} wordName
308
+ * @returns {object | null}
309
+ */
310
+ export function findWordItemInSnapshot (snap, wordName) {
311
+ if (!snap?.ok || !Array.isArray(snap.items)) return null
312
+ for (const it of snap.items) {
313
+ if (it.kind === 'word' && it.name === wordName) return it
314
+ }
315
+ return null
316
+ }
317
+
318
+ /**
319
+ * @param {{
320
+ * wordName: string
321
+ * wordAst: object | null
322
+ * wireSig: object | null | undefined
323
+ * exportAsync?: boolean
324
+ * asyncDefinition?: boolean
325
+ * mayFail?: boolean
326
+ * }} opts
327
+ * @returns {{ ok: true, block: string } | { ok: false, message: string }}
328
+ */
329
+ export function formatWordSailBlock ({
330
+ wordName,
331
+ wordAst,
332
+ wireSig,
333
+ exportAsync,
334
+ asyncDefinition,
335
+ mayFail
336
+ }) {
337
+ /** @type {string[]} */
338
+ const proseLines = []
339
+ if (wordAst && Array.isArray(wordAst.doc)) {
340
+ proseLines.push(...proseFromDocEntries(wordAst.doc))
341
+ }
342
+
343
+ let sigText
344
+ try {
345
+ if (wordAst?.signature) {
346
+ sigText = formatAstSignatureText(wordAst.signature)
347
+ } else if (wireSig) {
348
+ sigText = formatWireSignatureText(wireSig)
349
+ } else {
350
+ return { ok: false, message: `нет сигнатуры для @${wordName}` }
351
+ }
352
+ } catch (e) {
353
+ const msg = e instanceof Error ? e.message : String(e)
354
+ return { ok: false, message: msg }
355
+ }
356
+
357
+ const wantAsync =
358
+ exportAsync === true ||
359
+ asyncDefinition === true
360
+ if (wantAsync && !/\+Async\b/.test(sigText)) {
361
+ sigText = `${sigText} +Async`.trim()
362
+ }
363
+ if (mayFail === true && !/\+Fail\b/.test(sigText)) {
364
+ sigText = `${sigText} +Fail`.trim()
365
+ }
366
+
367
+ const sailDoc = formatDocBlocksAsSail(
368
+ wordAst && Array.isArray(wordAst.doc) ? wordAst.doc : []
369
+ )
370
+ const sailBody = [`@${wordName} ( ${sigText} )`, sailDoc]
371
+ .filter((s) => s.length > 0)
372
+ .join(' ')
373
+
374
+ try {
375
+ return {
376
+ ok: true,
377
+ block: formatJSDocSailBlock({ proseLines, sailText: sailBody })
378
+ }
379
+ } catch (e) {
380
+ const msg = e instanceof Error ? e.message : String(e)
381
+ return { ok: false, message: msg }
382
+ }
383
+ }