functionalscript 0.31.1 → 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.
@@ -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 descriptors ────────────────────────────────────────────────────────────
87
- const casAddTool = {
88
- name: 'cas_add',
89
- description: '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).',
90
- inputSchema: toJsonSchema(casAddArgs),
91
- };
92
- const casGetTool = {
93
- name: 'cas_get',
94
- description: '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.',
95
- inputSchema: toJsonSchema(casGetArgs),
96
- };
97
- const casListTool = {
98
- name: 'cas_list',
99
- description: 'List all stored content hashes (cBase32), one per line.',
100
- inputSchema: toJsonSchema(casListArgs),
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`
@@ -129,14 +129,6 @@ export const commands = [
129
129
  .step(forEachStep(j => log(vecToCBase32(j))))
130
130
  .step(() => pure(0));
131
131
  },
132
- },
133
- {
134
- names: ['mcp'],
135
- description: 'Run an MCP server over stdio exposing the CAS as tools',
136
- handler: ({ home }) => {
137
- const c = cas(sha256)(fileKvStore(home));
138
- return casMcpServer(c, hash => join(home, toPath(hash))).step(() => pure(0));
139
- },
140
- },
132
+ }
141
133
  ];
142
134
  export const main = dispatch(commands);
@@ -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 () => toVec(await readFile(path))),
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
- const server = asNominal(createServer(nodeRl));
195
- return server;
203
+ return asNominal(createServer(nodeRl));
196
204
  },
197
205
  listen: async (server, port) => {
198
206
  const s = asBase(server);
@@ -11,6 +11,7 @@ export declare const proof: {
11
11
  nested: () => void;
12
12
  noSuchFile: () => void;
13
13
  nestedPath: () => void;
14
+ withinLimit: () => void;
14
15
  };
15
16
  readUtf8File: {
16
17
  ok: () => void;
@@ -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: {
@@ -9,6 +9,11 @@ import { commands as casCommands } from "../cas/module.f.js";
9
9
  import { main as ciMain } from "../ci/module.f.js";
10
10
  import { import_ } from "../effects/node/module.f.js";
11
11
  import { dispatch } from "../cli/module.f.js";
12
+ import { casMcpServer } from "../cas/mcp/module.f.js";
13
+ import { cas, fileKvStore, toPath } from "../cas/module.f.js";
14
+ import { sha256 } from "../crypto/sha2/module.f.js";
15
+ import { join } from "../path/module.f.js";
16
+ import { pure } from "../effects/module.f.js";
12
17
  const commands = [
13
18
  {
14
19
  names: ['test', 't'],
@@ -25,6 +30,14 @@ const commands = [
25
30
  description: 'Content-addressable storage operations',
26
31
  handler: casCommands,
27
32
  },
33
+ {
34
+ names: ['mcp', 'm'],
35
+ description: 'Run an MCP server over stdio exposing the CAS as tools',
36
+ handler: ({ home }) => {
37
+ const c = cas(sha256)(fileKvStore(home));
38
+ return casMcpServer(c, hash => join(home, toPath(hash))).step(() => pure(0));
39
+ },
40
+ },
28
41
  {
29
42
  names: ['ci', 'i'],
30
43
  description: 'Generate the GitHub Actions CI workflow',
@@ -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`. */
@@ -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) => (1n << len) - 1n;
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) => u8ListToVecMsb(fromArrayLike(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
  /**
@@ -9,4 +9,6 @@ export declare const proof: {
9
9
  decodeUtf8Multibyte: () => void;
10
10
  utf8RoundTrip: () => void;
11
11
  listToVec: () => void;
12
+ maxLength: () => import("../bit_vec/module.f.ts").Vec;
13
+ throw: () => import("../bit_vec/module.f.ts").Vec;
12
14
  };
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "functionalscript",
3
- "version": "0.31.1",
3
+ "version": "0.32.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "**/*.js",
@@ -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
- };