functionalscript 0.32.0 → 0.32.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.
- package/fs/cas/mcp/module.f.js +84 -110
- package/fs/effects/node/module.f.d.ts +6 -0
- package/fs/effects/node/module.js +12 -4
- package/fs/effects/node/proof.f.d.ts +1 -0
- package/fs/effects/node/proof.f.js +16 -0
- package/fs/mcp/module.f.d.ts +46 -0
- package/fs/mcp/module.f.js +59 -0
- package/fs/types/bigint/module.f.d.ts +2 -0
- package/fs/types/bigint/module.f.js +10 -1
- package/fs/types/bit_vec/module.f.d.ts +31 -1
- package/fs/types/bit_vec/module.f.js +8 -1
- package/fs/types/uint8array/module.f.js +7 -2
- package/fs/types/uint8array/proof.f.d.ts +2 -0
- package/fs/types/uint8array/proof.f.js +4 -2
- package/nanvm-lib/tests/proof.f.d.ts +22 -0
- package/nanvm-lib/tests/proof.f.js +103 -0
- package/package.json +1 -1
- package/nanvm-lib/tests/vm/proof.f.d.ts +0 -24
- package/nanvm-lib/tests/vm/proof.f.js +0 -105
package/fs/cas/mcp/module.f.js
CHANGED
|
@@ -63,8 +63,6 @@
|
|
|
63
63
|
* @module
|
|
64
64
|
*/
|
|
65
65
|
import { string, option, or, boolean } from "../../types/rtti/module.f.js";
|
|
66
|
-
import { validate } from "../../types/rtti/validate/module.f.js";
|
|
67
|
-
import { toJsonSchema } from "../../json/schema/module.f.js";
|
|
68
66
|
import { pure } from "../../effects/module.f.js";
|
|
69
67
|
import { create } from "../../effects/memory/module.f.js";
|
|
70
68
|
import { cBase32ToVec, vecToCBase32 } from "../../cbase32/module.f.js";
|
|
@@ -74,7 +72,7 @@ import { detect } from "../../mime/module.f.js";
|
|
|
74
72
|
import { length as bitVecLength } from "../../types/bit_vec/module.f.js";
|
|
75
73
|
import { readFile } from "../../effects/node/module.f.js";
|
|
76
74
|
import { stdioTransport } from "../../mcp/stdio/module.f.js";
|
|
77
|
-
import { mcpStep, uninitializedState, } from "../../mcp/module.f.js";
|
|
75
|
+
import { mcpStep, uninitializedState, toolEntry, fromRegistry, errorResult, } from "../../mcp/module.f.js";
|
|
78
76
|
import { fromVec } from "../../text/utf8/module.f.js";
|
|
79
77
|
// ── Argument schemas (declared once, used for both inputSchema and validate) ─────
|
|
80
78
|
/** Arguments for `cas_add`: content to store, with optional encoding type. */
|
|
@@ -83,27 +81,91 @@ export const casAddArgs = { content: string, type: option(or(or('text', 'base64'
|
|
|
83
81
|
export const casGetArgs = { hash: string, content: option(boolean) };
|
|
84
82
|
/** Arguments for `cas_list`: none. */
|
|
85
83
|
export const casListArgs = {};
|
|
86
|
-
// ── Tool
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
84
|
+
// ── Tool registry ──────────────────────────────────────────────────────────────
|
|
85
|
+
/** Registry of all CAS tools. */
|
|
86
|
+
const casToolRegistry = (c, toUrl) => [
|
|
87
|
+
toolEntry('cas_add', 'Store content and return its hash (cBase32). Pass type:"base64" for binary; type:"url" to read from a filesystem path; omit or pass type:"text" for UTF-8 text (default).', casAddArgs, ({ type, content }) => {
|
|
88
|
+
let x;
|
|
89
|
+
switch (type) {
|
|
90
|
+
case 'url':
|
|
91
|
+
x = readFile(content).step(([t, v]) => pure(t === 'error'
|
|
92
|
+
? `cannot read file: ${content}: ${v}`
|
|
93
|
+
: v));
|
|
94
|
+
break;
|
|
95
|
+
case 'base64':
|
|
96
|
+
const value = base64Decode(content);
|
|
97
|
+
x = pure(value === null ? `invalid base64 content: ${content}` : value);
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
x = pure(utf8(content));
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
return x.step(value => typeof value === 'string'
|
|
104
|
+
? pure(errorResult(value))
|
|
105
|
+
: c.write(value).step(hash => pure(okResult(vecToCBase32(hash)))));
|
|
106
|
+
}),
|
|
107
|
+
toolEntry('cas_get', 'Inspect a blob by hash. Always returns JSON {length,mime_type,type[,url]} where type is "text" or "base64". Pass content:true to also include the inline content string.', casGetArgs, r => {
|
|
108
|
+
const key = cBase32ToVec(r.hash);
|
|
109
|
+
if (key === null) {
|
|
110
|
+
return pure(errorResult(`invalid cBase32 hash: ${r.hash}`));
|
|
111
|
+
}
|
|
112
|
+
return c.read(key).step(value => {
|
|
113
|
+
if (value === undefined) {
|
|
114
|
+
return pure(errorResult(`no such hash: ${r.hash}`));
|
|
115
|
+
}
|
|
116
|
+
const byteLength = Number(bitVecLength(value) / 8n);
|
|
117
|
+
// Phase 1: magic-byte sniffing for known binary formats.
|
|
118
|
+
const detectedMime = detect(value);
|
|
119
|
+
if (detectedMime !== null) {
|
|
120
|
+
const url = toUrl?.(key);
|
|
121
|
+
const meta = {
|
|
122
|
+
length: byteLength,
|
|
123
|
+
mime_type: detectedMime,
|
|
124
|
+
type: 'base64',
|
|
125
|
+
...(url !== undefined && { url })
|
|
126
|
+
};
|
|
127
|
+
if (r.content === true) {
|
|
128
|
+
const blob = base64Encode(value);
|
|
129
|
+
return pure(blob === null
|
|
130
|
+
? errorResult(`content is not byte-aligned: ${r.hash}`)
|
|
131
|
+
: okResult(JSON.stringify({ ...meta, content: blob })));
|
|
132
|
+
}
|
|
133
|
+
return pure(okResult(JSON.stringify(meta)));
|
|
134
|
+
}
|
|
135
|
+
// Phase 2: UTF-8 validation — text if valid, octet-stream otherwise.
|
|
136
|
+
const str = fromVec(value);
|
|
137
|
+
const url = toUrl?.(key);
|
|
138
|
+
if (str !== null) {
|
|
139
|
+
const meta = {
|
|
140
|
+
length: byteLength,
|
|
141
|
+
mime_type: 'text/plain',
|
|
142
|
+
type: 'text',
|
|
143
|
+
...(url !== undefined && { url })
|
|
144
|
+
};
|
|
145
|
+
return pure(r.content === true
|
|
146
|
+
? okResult(JSON.stringify({ ...meta, content: str }))
|
|
147
|
+
: okResult(JSON.stringify(meta)));
|
|
148
|
+
}
|
|
149
|
+
const meta = {
|
|
150
|
+
length: byteLength,
|
|
151
|
+
mime_type: 'application/octet-stream',
|
|
152
|
+
type: 'base64',
|
|
153
|
+
...(url !== undefined && { url })
|
|
154
|
+
};
|
|
155
|
+
if (r.content === true) {
|
|
156
|
+
const blob = base64Encode(value);
|
|
157
|
+
return pure(blob === null
|
|
158
|
+
? errorResult(`content is not byte-aligned: ${r.hash}`)
|
|
159
|
+
: okResult(JSON.stringify({ ...meta, content: blob })));
|
|
160
|
+
}
|
|
161
|
+
return pure(okResult(JSON.stringify(meta)));
|
|
162
|
+
});
|
|
163
|
+
}),
|
|
164
|
+
toolEntry('cas_list', 'List all stored content hashes (cBase32), one per line.', casListArgs, () => c.list().step(hashes => pure(okResult(hashes.map(vecToCBase32).join('\n'))))),
|
|
165
|
+
];
|
|
102
166
|
// ── Result helpers ──────────────────────────────────────────────────────────────
|
|
103
167
|
/** A successful single-text-block tool result. */
|
|
104
168
|
const okResult = (text) => ({ content: [{ type: 'text', text }] });
|
|
105
|
-
/** A tool-level failure: in-band `isError` result with a text explanation. */
|
|
106
|
-
const errorResult = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
107
169
|
// ── Handlers ────────────────────────────────────────────────────────────────────
|
|
108
170
|
/**
|
|
109
171
|
* MCP handlers for an injected `Cas<O>` — generic in `O` exactly like `Cas`
|
|
@@ -113,95 +175,7 @@ const errorResult = (text) => ({ content: [{ type: 'text', text }], isError: tru
|
|
|
113
175
|
* the blob on the local filesystem. When absent (e.g. memory-backed tests),
|
|
114
176
|
* `url` is omitted.
|
|
115
177
|
*/
|
|
116
|
-
export const casMcpHandlers = (c, toUrl) => (
|
|
117
|
-
toolsList: () => pure({ tools: [casAddTool, casGetTool, casListTool] }),
|
|
118
|
-
toolsCall: ({ name, arguments: args }) => {
|
|
119
|
-
const a = args === undefined ? {} : args;
|
|
120
|
-
switch (name) {
|
|
121
|
-
case 'cas_add': {
|
|
122
|
-
const [t, r] = validate(casAddArgs)(a);
|
|
123
|
-
if (t === 'error') {
|
|
124
|
-
return pure(errorResult(`invalid arguments: ${r.message}`));
|
|
125
|
-
}
|
|
126
|
-
const encoding = r.type ?? 'text';
|
|
127
|
-
if (encoding === 'url') {
|
|
128
|
-
return readFile(r.content).step(result => {
|
|
129
|
-
if (result[0] === 'error') {
|
|
130
|
-
return pure(errorResult(`cannot read file: ${r.content}: ${result[1]}`));
|
|
131
|
-
}
|
|
132
|
-
return c.write(result[1]).step(hash => pure(okResult(vecToCBase32(hash))));
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
let value;
|
|
136
|
-
if (encoding === 'base64') {
|
|
137
|
-
value = base64Decode(r.content);
|
|
138
|
-
if (value === null) {
|
|
139
|
-
return pure(errorResult(`invalid base64 content: ${r.content}`));
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
value = utf8(r.content);
|
|
144
|
-
}
|
|
145
|
-
return c.write(value).step(hash => pure(okResult(vecToCBase32(hash))));
|
|
146
|
-
}
|
|
147
|
-
case 'cas_get': {
|
|
148
|
-
const [t, r] = validate(casGetArgs)(a);
|
|
149
|
-
if (t === 'error') {
|
|
150
|
-
return pure(errorResult(`invalid arguments: ${r.message}`));
|
|
151
|
-
}
|
|
152
|
-
const key = cBase32ToVec(r.hash);
|
|
153
|
-
if (key === null) {
|
|
154
|
-
return pure(errorResult(`invalid cBase32 hash: ${r.hash}`));
|
|
155
|
-
}
|
|
156
|
-
return c.read(key).step(value => {
|
|
157
|
-
if (value === undefined) {
|
|
158
|
-
return pure(errorResult(`no such hash: ${r.hash}`));
|
|
159
|
-
}
|
|
160
|
-
const byteLength = Number(bitVecLength(value) / 8n);
|
|
161
|
-
// Phase 1: magic-byte sniffing for known binary formats.
|
|
162
|
-
const detectedMime = detect(value);
|
|
163
|
-
if (detectedMime !== null) {
|
|
164
|
-
const url = toUrl?.(key);
|
|
165
|
-
const meta = { length: byteLength, mime_type: detectedMime, type: 'base64', ...(url !== undefined && { url }) };
|
|
166
|
-
if (r.content === true) {
|
|
167
|
-
const blob = base64Encode(value);
|
|
168
|
-
if (blob === null) {
|
|
169
|
-
return pure(errorResult(`content is not byte-aligned: ${r.hash}`));
|
|
170
|
-
}
|
|
171
|
-
return pure(okResult(JSON.stringify({ ...meta, content: blob })));
|
|
172
|
-
}
|
|
173
|
-
return pure(okResult(JSON.stringify(meta)));
|
|
174
|
-
}
|
|
175
|
-
// Phase 2: UTF-8 validation — text if valid, octet-stream otherwise.
|
|
176
|
-
const str = fromVec(value);
|
|
177
|
-
const url = toUrl?.(key);
|
|
178
|
-
if (str !== null) {
|
|
179
|
-
const meta = { length: byteLength, mime_type: 'text/plain', type: 'text', ...(url !== undefined && { url }) };
|
|
180
|
-
if (r.content === true) {
|
|
181
|
-
return pure(okResult(JSON.stringify({ ...meta, content: str })));
|
|
182
|
-
}
|
|
183
|
-
return pure(okResult(JSON.stringify(meta)));
|
|
184
|
-
}
|
|
185
|
-
const meta = { length: byteLength, mime_type: 'application/octet-stream', type: 'base64', ...(url !== undefined && { url }) };
|
|
186
|
-
if (r.content === true) {
|
|
187
|
-
const blob = base64Encode(value);
|
|
188
|
-
if (blob === null) {
|
|
189
|
-
return pure(errorResult(`content is not byte-aligned: ${r.hash}`));
|
|
190
|
-
}
|
|
191
|
-
return pure(okResult(JSON.stringify({ ...meta, content: blob })));
|
|
192
|
-
}
|
|
193
|
-
return pure(okResult(JSON.stringify(meta)));
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
case 'cas_list': {
|
|
197
|
-
return c.list().step(hashes => pure(okResult(hashes.map(vecToCBase32).join('\n'))));
|
|
198
|
-
}
|
|
199
|
-
default: {
|
|
200
|
-
return pure(errorResult(`unknown tool: ${name}`));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
});
|
|
178
|
+
export const casMcpHandlers = (c, toUrl) => fromRegistry(casToolRegistry(c, toUrl));
|
|
205
179
|
// ── Session configuration ───────────────────────────────────────────────────────
|
|
206
180
|
/**
|
|
207
181
|
* Static MCP configuration for the CAS server: advertises the `tools`
|
|
@@ -31,6 +31,12 @@ export type MakeDirectoryOptions = {
|
|
|
31
31
|
};
|
|
32
32
|
export type Mkdir = readonly ['mkdir', (path: string, options?: MakeDirectoryOptions) => IoResult<void>];
|
|
33
33
|
export declare const mkdir: Func<Mkdir>;
|
|
34
|
+
/**
|
|
35
|
+
* Reads a file as a bit vector. File size is limited to 131,072 bytes (128 KiB)
|
|
36
|
+
* to respect Bun's `bigint` size constraint (1,048,575 bits), which is the
|
|
37
|
+
* minimal limit across all runtime environments supported by FunctionalScript.
|
|
38
|
+
* Files exceeding this limit will fail with a validation error.
|
|
39
|
+
*/
|
|
34
40
|
export type ReadFile = readonly ['readFile', (path: string) => IoResult<Vec>];
|
|
35
41
|
export declare const readFile: Func<ReadFile>;
|
|
36
42
|
/**
|
|
@@ -35,6 +35,7 @@ import {} from "./module.f.js";
|
|
|
35
35
|
import { asBase, asNominal } from "../../types/nominal/module.f.js";
|
|
36
36
|
import { error, ok } from "../../types/result/module.f.js";
|
|
37
37
|
import { fromVec, listToVec, toVec } from "../../types/uint8array/module.f.js";
|
|
38
|
+
import { maxLengthBytes } from "../../types/bit_vec/module.f.js";
|
|
38
39
|
/**
|
|
39
40
|
* Narrowed structural view of `node:http`'s `createServer`. The official types
|
|
40
41
|
* declare `method`/`url` optional and header values as
|
|
@@ -57,8 +58,9 @@ const collect = async (v) => {
|
|
|
57
58
|
}
|
|
58
59
|
return result;
|
|
59
60
|
};
|
|
60
|
-
const { mkdir, readFile, readdir, writeFile, rm, access } = fs.promises;
|
|
61
|
+
const { mkdir, readFile, readdir, writeFile, rm, access, stat } = fs.promises;
|
|
61
62
|
const { exec } = childProcess;
|
|
63
|
+
const maxFileSizeBytes = Number(maxLengthBytes);
|
|
62
64
|
const prefix = 'file:///';
|
|
63
65
|
const asyncImport = (v) => {
|
|
64
66
|
const s0 = v.includes(':') || v.startsWith('/') ? v : concat(process.cwd())(v);
|
|
@@ -161,7 +163,14 @@ const runNodeEffect = asyncRun({
|
|
|
161
163
|
return toVec(new Uint8Array(await response.arrayBuffer()));
|
|
162
164
|
}),
|
|
163
165
|
mkdir: (...p) => tc(async () => { await mkdir(...p); }),
|
|
164
|
-
readFile: path => tc(async () =>
|
|
166
|
+
readFile: path => tc(async () => {
|
|
167
|
+
const fileStats = await stat(path);
|
|
168
|
+
// if the file is too big, toVec should fail anyway but in this case we don't want to load the file.
|
|
169
|
+
if (fileStats.size > maxFileSizeBytes) {
|
|
170
|
+
throw new Error(`File size ${fileStats.size} exceeds maximum allowed size of ${Number(maxFileSizeBytes)} bytes`);
|
|
171
|
+
}
|
|
172
|
+
return toVec(await readFile(path));
|
|
173
|
+
}),
|
|
165
174
|
readdir: (path, r) => tc(async () => (await readdir(path, { ...r, withFileTypes: true }))
|
|
166
175
|
.map(v => ({
|
|
167
176
|
name: v.name,
|
|
@@ -191,8 +200,7 @@ const runNodeEffect = asyncRun({
|
|
|
191
200
|
.writeHead(status, outHeaders)
|
|
192
201
|
.end(fromVec(outBody));
|
|
193
202
|
};
|
|
194
|
-
|
|
195
|
-
return server;
|
|
203
|
+
return asNominal(createServer(nodeRl));
|
|
196
204
|
},
|
|
197
205
|
listen: async (server, port) => {
|
|
198
206
|
const s = asBase(server);
|
|
@@ -127,6 +127,22 @@ export const proof = {
|
|
|
127
127
|
if (result.code !== 'ENOENT') {
|
|
128
128
|
throw result;
|
|
129
129
|
}
|
|
130
|
+
},
|
|
131
|
+
withinLimit: () => {
|
|
132
|
+
// Test with a small file well within the 131,072 byte limit
|
|
133
|
+
const initial = {
|
|
134
|
+
...emptyState,
|
|
135
|
+
root: {
|
|
136
|
+
smallFile: vec8(0x2an),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const [_, [t, result]] = virtual(initial)(readFile('smallFile'));
|
|
140
|
+
if (t === 'error') {
|
|
141
|
+
throw result;
|
|
142
|
+
}
|
|
143
|
+
if (!isVec(result)) {
|
|
144
|
+
throw result;
|
|
145
|
+
}
|
|
130
146
|
}
|
|
131
147
|
},
|
|
132
148
|
readUtf8File: {
|
package/fs/mcp/module.f.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Ts } from '../types/rtti/ts/module.f.ts';
|
|
|
3
3
|
import { type Operation, type Effect } from '../effects/module.f.ts';
|
|
4
4
|
import { type Key, type MemOp } from '../effects/memory/module.f.ts';
|
|
5
5
|
import { type Response } from '../json/rpc/module.f.ts';
|
|
6
|
+
import type { Type } from '../types/rtti/module.f.ts';
|
|
6
7
|
/** Name + version pair sent in `initialize` requests and responses. */
|
|
7
8
|
export declare const implementation: {
|
|
8
9
|
readonly name: import("../types/rtti/module.f.ts").String;
|
|
@@ -139,6 +140,51 @@ export type McpHandlers<O extends Operation> = {
|
|
|
139
140
|
readonly toolsList: (params: ToolsListParams) => Effect<O, ToolsListResult>;
|
|
140
141
|
readonly toolsCall: (params: ToolsCallParams) => Effect<O, ToolsCallResult>;
|
|
141
142
|
};
|
|
143
|
+
/**
|
|
144
|
+
* A single declarative tool entry combining metadata, input schema, and type-safe handler.
|
|
145
|
+
*
|
|
146
|
+
* The handler receives pre-validated arguments of type `Ts<inputRtti>`, eliminating the need
|
|
147
|
+
* for manual validation or type casting. All validation is encapsulated in the entry.
|
|
148
|
+
*/
|
|
149
|
+
export type ToolEntry<O extends Operation> = {
|
|
150
|
+
readonly name: string;
|
|
151
|
+
readonly description: string;
|
|
152
|
+
readonly inputRtti: Type;
|
|
153
|
+
readonly handle: (args: Unknown) => Effect<O, ToolsCallResult>;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Creates a type-safe tool entry that binds an RTTI schema with a handler.
|
|
157
|
+
*
|
|
158
|
+
* The builder validates arguments at runtime using the RTTI and passes pre-validated
|
|
159
|
+
* arguments (typed as `Ts<T>`) to the handler. This eliminates manual validation
|
|
160
|
+
* boilerplate and type assertions.
|
|
161
|
+
*
|
|
162
|
+
* @param name - The tool name (used in `tools/call` requests)
|
|
163
|
+
* @param description - Human-readable description for `tools/list`
|
|
164
|
+
* @param inputRtti - Runtime type info for input validation
|
|
165
|
+
* @param handle - Handler receiving validated arguments of type `Ts<inputRtti>`
|
|
166
|
+
* @returns A `ToolEntry` ready to be added to a registry
|
|
167
|
+
*/
|
|
168
|
+
export declare const toolEntry: <T extends Type, O extends Operation>(name: string, description: string, inputRtti: T, handle: (args: Ts<T>) => Effect<O, ToolsCallResult>) => ToolEntry<O>;
|
|
169
|
+
/**
|
|
170
|
+
* Helper to create a tool-level error result with plain text explanation.
|
|
171
|
+
*
|
|
172
|
+
* @param text - The error message to return to the client
|
|
173
|
+
* @returns A `ToolsCallResult` with `isError: true` and the text explanation
|
|
174
|
+
*/
|
|
175
|
+
export declare const errorResult: (text: string) => ToolsCallResult;
|
|
176
|
+
/**
|
|
177
|
+
* Builds `McpHandlers` from a registry of tool entries.
|
|
178
|
+
*
|
|
179
|
+
* This factory generates `toolsList` and `toolsCall` handlers that work with a
|
|
180
|
+
* declarative registry, eliminating boilerplate. The `toolsList` handler converts
|
|
181
|
+
* entries into MCP `Tool` descriptors, and `toolsCall` dispatches by name and
|
|
182
|
+
* delegates to the appropriate handler.
|
|
183
|
+
*
|
|
184
|
+
* @param registry - Array of tool entries
|
|
185
|
+
* @returns Complete `McpHandlers` ready for use with `mcpStep`
|
|
186
|
+
*/
|
|
187
|
+
export declare const fromRegistry: <O extends Operation>(registry: readonly ToolEntry<O>[]) => McpHandlers<O>;
|
|
142
188
|
/** Top-level handler: maps a raw JSON value to a JSON-RPC response (or `null` for notifications). */
|
|
143
189
|
export type Handle<O extends Operation> = (value: Unknown) => Effect<O, Response | null>;
|
|
144
190
|
/** MCP error -32002: the client called a method before `initialize`. */
|
package/fs/mcp/module.f.js
CHANGED
|
@@ -19,6 +19,7 @@ import { pure } from "../effects/module.f.js";
|
|
|
19
19
|
import { read, write } from "../effects/memory/module.f.js";
|
|
20
20
|
import { decodeRequest, rpcError, invalidRequest, invalidParams, methodNotFound, jsonrpc, } from "../json/rpc/module.f.js";
|
|
21
21
|
import { validate } from "../types/rtti/validate/module.f.js";
|
|
22
|
+
import { toJsonSchema } from "../json/schema/module.f.js";
|
|
22
23
|
// ── Shared ─────────────────────────────────────────────────────────────────────
|
|
23
24
|
/** Name + version pair sent in `initialize` requests and responses. */
|
|
24
25
|
export const implementation = {
|
|
@@ -100,6 +101,64 @@ export const toolsCallResult = {
|
|
|
100
101
|
content: array(contentItem),
|
|
101
102
|
isError: option(boolean),
|
|
102
103
|
};
|
|
104
|
+
/**
|
|
105
|
+
* Creates a type-safe tool entry that binds an RTTI schema with a handler.
|
|
106
|
+
*
|
|
107
|
+
* The builder validates arguments at runtime using the RTTI and passes pre-validated
|
|
108
|
+
* arguments (typed as `Ts<T>`) to the handler. This eliminates manual validation
|
|
109
|
+
* boilerplate and type assertions.
|
|
110
|
+
*
|
|
111
|
+
* @param name - The tool name (used in `tools/call` requests)
|
|
112
|
+
* @param description - Human-readable description for `tools/list`
|
|
113
|
+
* @param inputRtti - Runtime type info for input validation
|
|
114
|
+
* @param handle - Handler receiving validated arguments of type `Ts<inputRtti>`
|
|
115
|
+
* @returns A `ToolEntry` ready to be added to a registry
|
|
116
|
+
*/
|
|
117
|
+
export const toolEntry = (name, description, inputRtti, handle) => ({
|
|
118
|
+
name,
|
|
119
|
+
description,
|
|
120
|
+
inputRtti,
|
|
121
|
+
handle: (a) => {
|
|
122
|
+
const [t, r] = validate(inputRtti)(a);
|
|
123
|
+
return t === 'error'
|
|
124
|
+
? pure(errorResult(`invalid arguments: ${r.message}`))
|
|
125
|
+
: handle(r);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
/**
|
|
129
|
+
* Helper to create a tool-level error result with plain text explanation.
|
|
130
|
+
*
|
|
131
|
+
* @param text - The error message to return to the client
|
|
132
|
+
* @returns A `ToolsCallResult` with `isError: true` and the text explanation
|
|
133
|
+
*/
|
|
134
|
+
export const errorResult = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
135
|
+
/**
|
|
136
|
+
* Builds `McpHandlers` from a registry of tool entries.
|
|
137
|
+
*
|
|
138
|
+
* This factory generates `toolsList` and `toolsCall` handlers that work with a
|
|
139
|
+
* declarative registry, eliminating boilerplate. The `toolsList` handler converts
|
|
140
|
+
* entries into MCP `Tool` descriptors, and `toolsCall` dispatches by name and
|
|
141
|
+
* delegates to the appropriate handler.
|
|
142
|
+
*
|
|
143
|
+
* @param registry - Array of tool entries
|
|
144
|
+
* @returns Complete `McpHandlers` ready for use with `mcpStep`
|
|
145
|
+
*/
|
|
146
|
+
export const fromRegistry = (registry) => ({
|
|
147
|
+
toolsList: () => {
|
|
148
|
+
const tools = registry.map(entry => ({
|
|
149
|
+
name: entry.name,
|
|
150
|
+
description: entry.description,
|
|
151
|
+
inputSchema: toJsonSchema(entry.inputRtti),
|
|
152
|
+
}));
|
|
153
|
+
return pure({ tools });
|
|
154
|
+
},
|
|
155
|
+
toolsCall: ({ name, arguments: args }) => {
|
|
156
|
+
const entry = registry.find(e => e.name === name);
|
|
157
|
+
return entry === undefined
|
|
158
|
+
? pure(errorResult(`unknown tool: ${name}`))
|
|
159
|
+
: entry.handle(args === undefined ? {} : args);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
103
162
|
// ── Lifecycle / capability state machine ───────────────────────────────────────
|
|
104
163
|
const _errResponse = (id) => (error) => ({ jsonrpc, error, id });
|
|
105
164
|
const _okResponse = (id) => (result) => ({ jsonrpc, result, id });
|
|
@@ -129,6 +129,8 @@ export declare const bitLength: (v: bigint) => bigint;
|
|
|
129
129
|
* ```
|
|
130
130
|
*/
|
|
131
131
|
export declare const mask: (len: bigint) => bigint;
|
|
132
|
+
export declare const maxLength = 1048576n;
|
|
133
|
+
export declare const max: bigint;
|
|
132
134
|
/**
|
|
133
135
|
* Calculates the partial factorial `b!/a!`.
|
|
134
136
|
*
|
|
@@ -172,7 +172,16 @@ export const bitLength = (v) => log2(abs(v)) + 1n;
|
|
|
172
172
|
* const result = mask(3n) // 7n
|
|
173
173
|
* ```
|
|
174
174
|
*/
|
|
175
|
-
export const mask = (len) =>
|
|
175
|
+
export const mask = (len) => {
|
|
176
|
+
// we compute this way to avoid overflowing in Bun when len === maxLength.
|
|
177
|
+
const r = len & 1n;
|
|
178
|
+
const h = len >> 1n;
|
|
179
|
+
const x = (((1n << h) - 1n) << r) | r;
|
|
180
|
+
return (x << h) | x;
|
|
181
|
+
};
|
|
182
|
+
export const maxLength = 0x100000n;
|
|
183
|
+
// max + 1n // bun throws an error
|
|
184
|
+
export const max = mask(maxLength);
|
|
176
185
|
/**
|
|
177
186
|
* Calculates the partial factorial `b!/a!`.
|
|
178
187
|
*
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bit vectors that normalize the most-significant bit using signed `bigint` values.
|
|
3
|
+
*
|
|
4
|
+
* A value whose top bit is already set remains positive, while other values are
|
|
5
|
+
* negated after toggling the leading bit so the stop bit is always `1`. The sign bit
|
|
6
|
+
* therefore acts as the stop bit that encodes the logical length of the vector.
|
|
7
|
+
*
|
|
8
|
+
* MSb is most-significant bit first.
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* - byte: 0x53 = 0b0101_0011
|
|
12
|
+
* - 0123_4567
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* LSb is least-significant bit first.
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* - byte: 0x53 = 0b0101_0011
|
|
19
|
+
* - 7654_3210
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import { maxLength } from '../bigint/module.f.ts';
|
|
1
25
|
import type { Binary, Fold, Reduce as OpReduce } from '../function/operator/module.f.ts';
|
|
2
26
|
import { type List, type Thunk } from '../list/module.f.ts';
|
|
3
27
|
import { type Nominal } from '../nominal/module.f.ts';
|
|
@@ -6,6 +30,13 @@ import { type Sign } from '../function/compare/module.f.ts';
|
|
|
6
30
|
* A vector of bits represented as a signed `bigint`.
|
|
7
31
|
*/
|
|
8
32
|
export type Vec = Nominal<'bit_vec', '1a23a4336197e6158b6936cad34e90d146cd84b9b40ff7ab75a17c6d79e31d89', bigint>;
|
|
33
|
+
/**
|
|
34
|
+
* Maximum length of a bit vector in bits (1_048_576 = 0x10_0000).
|
|
35
|
+
* This limit is enforced by Bun's `bigint` size constraint, the minimal limit
|
|
36
|
+
* across all runtime environments supported by FunctionalScript.
|
|
37
|
+
*/
|
|
38
|
+
export { maxLength };
|
|
39
|
+
export declare const maxLengthBytes: bigint;
|
|
9
40
|
/**
|
|
10
41
|
* An empty vector of bits.
|
|
11
42
|
*/
|
|
@@ -251,4 +282,3 @@ export declare const u8List: (bo: BitOrder) => (v: Vec) => Thunk<number>;
|
|
|
251
282
|
*/
|
|
252
283
|
export declare const repeat: Fold<bigint, Vec>;
|
|
253
284
|
export declare const isVec: <T>(v: Vec | T) => v is Vec;
|
|
254
|
-
export {};
|
|
@@ -21,12 +21,19 @@
|
|
|
21
21
|
*
|
|
22
22
|
* @module
|
|
23
23
|
*/
|
|
24
|
-
import { bitLength, divUp, mask, xor } from "../bigint/module.f.js";
|
|
24
|
+
import { bitLength, divUp, mask, maxLength, xor } from "../bigint/module.f.js";
|
|
25
25
|
import { flip, identity } from "../function/module.f.js";
|
|
26
26
|
import { fold, iterable, map } from "../list/module.f.js";
|
|
27
27
|
import { asBase, asNominal } from "../nominal/module.f.js";
|
|
28
28
|
import { repeat as mRepeat } from "../monoid/module.f.js";
|
|
29
29
|
import { cmp, max, min } from "../function/compare/module.f.js";
|
|
30
|
+
/**
|
|
31
|
+
* Maximum length of a bit vector in bits (1_048_576 = 0x10_0000).
|
|
32
|
+
* This limit is enforced by Bun's `bigint` size constraint, the minimal limit
|
|
33
|
+
* across all runtime environments supported by FunctionalScript.
|
|
34
|
+
*/
|
|
35
|
+
export { maxLength };
|
|
36
|
+
export const maxLengthBytes = maxLength >> 3n;
|
|
30
37
|
/**
|
|
31
38
|
* An empty vector of bits.
|
|
32
39
|
*/
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
12
|
import { utf8, utf8ToString } from "../../text/module.f.js";
|
|
13
|
-
import { msb, u8List, u8ListToVec } from "../bit_vec/module.f.js";
|
|
13
|
+
import { maxLengthBytes, msb, u8List, u8ListToVec } from "../bit_vec/module.f.js";
|
|
14
14
|
import { compose } from "../function/module.f.js";
|
|
15
15
|
import { flat, fromArrayLike, iterable, map } from "../list/module.f.js";
|
|
16
16
|
const u8ListToVecMsb = u8ListToVec(msb);
|
|
@@ -18,7 +18,12 @@ const u8ListMsb = u8List(msb);
|
|
|
18
18
|
/**
|
|
19
19
|
* Converts a Uint8Array into an MSB-first bit vector.
|
|
20
20
|
*/
|
|
21
|
-
export const toVec = (input) =>
|
|
21
|
+
export const toVec = (input) => {
|
|
22
|
+
if (input.length > maxLengthBytes) {
|
|
23
|
+
throw "the array is too big";
|
|
24
|
+
}
|
|
25
|
+
return u8ListToVecMsb(fromArrayLike(input));
|
|
26
|
+
};
|
|
22
27
|
const m = map(fromArrayLike);
|
|
23
28
|
export const listToVec = (input) => u8ListToVecMsb(flat(m(input)));
|
|
24
29
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { vec } from "../bit_vec/module.f.js";
|
|
1
|
+
import { maxLength, maxLengthBytes, vec } from "../bit_vec/module.f.js";
|
|
2
2
|
import { toVec, fromVec, listToVec, decodeUtf8, encodeUtf8 } from "./module.f.js";
|
|
3
3
|
import { strictEqual } from "../function/operator/module.f.js";
|
|
4
4
|
import { equal, fromArrayLike } from "../list/module.f.js";
|
|
@@ -58,5 +58,7 @@ export const proof = {
|
|
|
58
58
|
listToVec: () => {
|
|
59
59
|
const result = listToVec([Uint8Array.from([1, 2]), Uint8Array.from([3])]);
|
|
60
60
|
assertArrayEq(fromVec(result), Uint8Array.from([1, 2, 3]));
|
|
61
|
-
}
|
|
61
|
+
},
|
|
62
|
+
maxLength: () => toVec(new Uint8Array(Number(maxLengthBytes))),
|
|
63
|
+
throw: () => toVec(new Uint8Array(Number(maxLengthBytes) + 1)),
|
|
62
64
|
};
|
|
@@ -88,4 +88,26 @@ export declare const proof: {
|
|
|
88
88
|
};
|
|
89
89
|
function: () => void;
|
|
90
90
|
};
|
|
91
|
+
stringCoercion: {
|
|
92
|
+
number: () => void;
|
|
93
|
+
bool: () => void;
|
|
94
|
+
null: () => void;
|
|
95
|
+
undefined: () => void;
|
|
96
|
+
bigint: () => void;
|
|
97
|
+
array: () => void;
|
|
98
|
+
func: () => void;
|
|
99
|
+
object: {
|
|
100
|
+
norm: () => void;
|
|
101
|
+
toString: () => void;
|
|
102
|
+
toStringThrow: {
|
|
103
|
+
throw: () => void;
|
|
104
|
+
};
|
|
105
|
+
toStringNotFunc: {
|
|
106
|
+
throw: () => void;
|
|
107
|
+
};
|
|
108
|
+
toStringNonPrimitive: {
|
|
109
|
+
throw: () => void;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
};
|
|
91
113
|
};
|
|
@@ -12,6 +12,7 @@ const nanRes = (op) => (n) => {
|
|
|
12
12
|
throw result;
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
|
+
const stringCoercion = String;
|
|
15
16
|
export const proof = {
|
|
16
17
|
eq: () => {
|
|
17
18
|
const e = (a) => (b) => {
|
|
@@ -172,5 +173,107 @@ export const proof = {
|
|
|
172
173
|
},
|
|
173
174
|
function: () => nan(op(() => { }))
|
|
174
175
|
};
|
|
176
|
+
},
|
|
177
|
+
stringCoercion: {
|
|
178
|
+
number: () => {
|
|
179
|
+
if (stringCoercion(123) !== '123') {
|
|
180
|
+
throw [123, 'toString', '123'];
|
|
181
|
+
}
|
|
182
|
+
if (stringCoercion(-456) !== '-456') {
|
|
183
|
+
throw [-456, 'toString', '-456'];
|
|
184
|
+
}
|
|
185
|
+
if (stringCoercion(0) !== '0') {
|
|
186
|
+
throw [0, 'toString', '0'];
|
|
187
|
+
}
|
|
188
|
+
if (stringCoercion(-0) !== '0') {
|
|
189
|
+
throw [0, 'toString', '0'];
|
|
190
|
+
}
|
|
191
|
+
if (stringCoercion(1 / (-0)) !== '-Infinity') {
|
|
192
|
+
throw [0, 'toString', '-Infinity'];
|
|
193
|
+
}
|
|
194
|
+
if (stringCoercion(Infinity) !== 'Infinity') {
|
|
195
|
+
throw [Infinity, 'toString', 'Infinity'];
|
|
196
|
+
}
|
|
197
|
+
if (stringCoercion(-Infinity) !== '-Infinity') {
|
|
198
|
+
throw [-Infinity, 'toString', '-Infinity'];
|
|
199
|
+
}
|
|
200
|
+
if (stringCoercion(1 / -Infinity) !== '0') {
|
|
201
|
+
throw [-Infinity, 'toString', '0'];
|
|
202
|
+
}
|
|
203
|
+
if (stringCoercion(NaN) !== 'NaN') {
|
|
204
|
+
throw [NaN, 'toString', 'NaN'];
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
bool: () => {
|
|
208
|
+
if (stringCoercion(true) !== 'true') {
|
|
209
|
+
throw [true, 'toString', 'true'];
|
|
210
|
+
}
|
|
211
|
+
if (stringCoercion(false) !== 'false') {
|
|
212
|
+
throw [false, 'toString', 'false'];
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
null: () => {
|
|
216
|
+
if (stringCoercion(null) !== 'null') {
|
|
217
|
+
throw [null, 'toString', 'null'];
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
undefined: () => {
|
|
221
|
+
if (stringCoercion(undefined) !== 'undefined') {
|
|
222
|
+
throw [undefined, 'toString', 'undefined'];
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
bigint: () => {
|
|
226
|
+
if (stringCoercion(123n) !== '123') {
|
|
227
|
+
throw [123n, 'toString', '123'];
|
|
228
|
+
}
|
|
229
|
+
if (stringCoercion(-456n) !== '-456') {
|
|
230
|
+
throw [-456n, 'toString', '-456'];
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
array: () => {
|
|
234
|
+
const arr = [1, 2, 3];
|
|
235
|
+
if (stringCoercion(arr) !== '1,2,3') {
|
|
236
|
+
throw [arr, 'toString', '1,2,3'];
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
func: () => {
|
|
240
|
+
const func = () => 5;
|
|
241
|
+
if (typeof stringCoercion(func) !== 'string') {
|
|
242
|
+
throw [func, 'toString'];
|
|
243
|
+
}
|
|
244
|
+
// if (stringCoercion(func) !== '() => 5') { throw [func, 'toString', 'function result'] }
|
|
245
|
+
},
|
|
246
|
+
object: {
|
|
247
|
+
norm: () => {
|
|
248
|
+
const obj = { a: 1, b: 2 };
|
|
249
|
+
if (stringCoercion(obj) !== '[object Object]') {
|
|
250
|
+
throw [obj, 'toString', '[object Object]'];
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
toString: () => {
|
|
254
|
+
const x = { toString: () => 'custom string' };
|
|
255
|
+
if (stringCoercion(x) !== 'custom string') {
|
|
256
|
+
throw [x, 'toString', 'custom string'];
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
toStringThrow: {
|
|
260
|
+
throw: () => {
|
|
261
|
+
const x = { toString: () => { throw new Error('Custom error'); } };
|
|
262
|
+
stringCoercion(x);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
toStringNotFunc: {
|
|
266
|
+
throw: () => {
|
|
267
|
+
const x = { toString: 'hello' };
|
|
268
|
+
stringCoercion(x);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
toStringNonPrimitive: {
|
|
272
|
+
throw: () => {
|
|
273
|
+
const x = { toString: () => [] };
|
|
274
|
+
stringCoercion(x);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
175
278
|
}
|
|
176
279
|
};
|
package/package.json
CHANGED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export declare const proof: {
|
|
2
|
-
stringCoercion: {
|
|
3
|
-
number: () => void;
|
|
4
|
-
bool: () => void;
|
|
5
|
-
null: () => void;
|
|
6
|
-
undefined: () => void;
|
|
7
|
-
bigint: () => void;
|
|
8
|
-
array: () => void;
|
|
9
|
-
func: () => void;
|
|
10
|
-
object: {
|
|
11
|
-
norm: () => void;
|
|
12
|
-
toString: () => void;
|
|
13
|
-
toStringThrow: {
|
|
14
|
-
throw: () => void;
|
|
15
|
-
};
|
|
16
|
-
toStringNotFunc: {
|
|
17
|
-
throw: () => void;
|
|
18
|
-
};
|
|
19
|
-
toStringNonPrimitive: {
|
|
20
|
-
throw: () => void;
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
};
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
const stringCoercion = String;
|
|
2
|
-
export const proof = {
|
|
3
|
-
stringCoercion: {
|
|
4
|
-
number: () => {
|
|
5
|
-
if (stringCoercion(123) !== '123') {
|
|
6
|
-
throw [123, 'toString', '123'];
|
|
7
|
-
}
|
|
8
|
-
if (stringCoercion(-456) !== '-456') {
|
|
9
|
-
throw [-456, 'toString', '-456'];
|
|
10
|
-
}
|
|
11
|
-
if (stringCoercion(0) !== '0') {
|
|
12
|
-
throw [0, 'toString', '0'];
|
|
13
|
-
}
|
|
14
|
-
if (stringCoercion(-0) !== '0') {
|
|
15
|
-
throw [0, 'toString', '0'];
|
|
16
|
-
}
|
|
17
|
-
if (stringCoercion(1 / (-0)) !== '-Infinity') {
|
|
18
|
-
throw [0, 'toString', '-Infinity'];
|
|
19
|
-
}
|
|
20
|
-
if (stringCoercion(Infinity) !== 'Infinity') {
|
|
21
|
-
throw [Infinity, 'toString', 'Infinity'];
|
|
22
|
-
}
|
|
23
|
-
if (stringCoercion(-Infinity) !== '-Infinity') {
|
|
24
|
-
throw [-Infinity, 'toString', '-Infinity'];
|
|
25
|
-
}
|
|
26
|
-
if (stringCoercion(1 / -Infinity) !== '0') {
|
|
27
|
-
throw [-Infinity, 'toString', '0'];
|
|
28
|
-
}
|
|
29
|
-
if (stringCoercion(NaN) !== 'NaN') {
|
|
30
|
-
throw [NaN, 'toString', 'NaN'];
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
bool: () => {
|
|
34
|
-
if (stringCoercion(true) !== 'true') {
|
|
35
|
-
throw [true, 'toString', 'true'];
|
|
36
|
-
}
|
|
37
|
-
if (stringCoercion(false) !== 'false') {
|
|
38
|
-
throw [false, 'toString', 'false'];
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
null: () => {
|
|
42
|
-
if (stringCoercion(null) !== 'null') {
|
|
43
|
-
throw [null, 'toString', 'null'];
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
undefined: () => {
|
|
47
|
-
if (stringCoercion(undefined) !== 'undefined') {
|
|
48
|
-
throw [undefined, 'toString', 'undefined'];
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
bigint: () => {
|
|
52
|
-
if (stringCoercion(123n) !== '123') {
|
|
53
|
-
throw [123n, 'toString', '123'];
|
|
54
|
-
}
|
|
55
|
-
if (stringCoercion(-456n) !== '-456') {
|
|
56
|
-
throw [-456n, 'toString', '-456'];
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
array: () => {
|
|
60
|
-
const arr = [1, 2, 3];
|
|
61
|
-
if (stringCoercion(arr) !== '1,2,3') {
|
|
62
|
-
throw [arr, 'toString', '1,2,3'];
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
func: () => {
|
|
66
|
-
const func = () => 5;
|
|
67
|
-
if (typeof stringCoercion(func) !== 'string') {
|
|
68
|
-
throw [func, 'toString'];
|
|
69
|
-
}
|
|
70
|
-
// if (stringCoercion(func) !== '() => 5') { throw [func, 'toString', 'function result'] }
|
|
71
|
-
},
|
|
72
|
-
object: {
|
|
73
|
-
norm: () => {
|
|
74
|
-
const obj = { a: 1, b: 2 };
|
|
75
|
-
if (stringCoercion(obj) !== '[object Object]') {
|
|
76
|
-
throw [obj, 'toString', '[object Object]'];
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
toString: () => {
|
|
80
|
-
const x = { toString: () => 'custom string' };
|
|
81
|
-
if (stringCoercion(x) !== 'custom string') {
|
|
82
|
-
throw [x, 'toString', 'custom string'];
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
toStringThrow: {
|
|
86
|
-
throw: () => {
|
|
87
|
-
const x = { toString: () => { throw new Error('Custom error'); } };
|
|
88
|
-
stringCoercion(x);
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
toStringNotFunc: {
|
|
92
|
-
throw: () => {
|
|
93
|
-
const x = { toString: 'hello' };
|
|
94
|
-
stringCoercion(x);
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
toStringNonPrimitive: {
|
|
98
|
-
throw: () => {
|
|
99
|
-
const x = { toString: () => [] };
|
|
100
|
-
stringCoercion(x);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|