@typesol/cli 0.24.0 → 0.24.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/CHANGELOG.md CHANGED
@@ -1,10 +1,24 @@
1
1
  # Changelog
2
2
 
3
- TypeSOL follows [semantic versioning](https://semver.org/) with the 0.x rules described in CONTRIBUTING.md:
4
- until 1.0, a minor version may change the language, the IDL layout or error codes, and says so here.
3
+ TypeSOL follows the organization's [versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md)
4
+ (compiler specifics in CONTRIBUTING.md): until 1.0, a minor version may change the language, the IDL layout or error
5
+ codes, and says so here.
5
6
 
6
7
  ## Unreleased
7
8
 
9
+ ## 0.24.1 — 2026-10-28
10
+
11
+ Smaller programs and fewer compute units, with the same results; no change to the language, IDLs or error codes. Also
12
+ new contributor docs (repository layout, a guide to the C runtime). Not externally audited (see SECURITY.md).
13
+
14
+ ### Performance
15
+ - Smaller programs, same results: the rent calculation no longer links soft-float (an exact integer equivalent,
16
+ checked against IEEE doubles), and every failure logs through one shared call. The benchmarked programs are 11.6%
17
+ smaller in total (counter 13.1 → 11.0 KB, escrow 23.9 → 21.3 KB). A failing instruction costs 2 more compute units;
18
+ successful ones are unchanged.
19
+ - An identical signer-seed list in the same scope reuses the array declared for the first CPI (escrow `take_offer`
20
+ 7058 → 7044, `refund_offer` 4287 → 4271).
21
+
8
22
  ## 0.24.0 — 2026-10-28
9
23
 
10
24
  Functions as values, closures for any helper, stored `map` to structs and text, Metaplex Core external plugins, and
package/README.md CHANGED
@@ -232,7 +232,9 @@ Subpath exports: `typesol/compiler`, `typesol/workspace`, `typesol/cpi`, `typeso
232
232
 
233
233
  The same counter, vault and escrow programs, written four ways. All four use the same instruction encoding, account
234
234
  layout and checks. They get identical transactions in LiteSVM and must end in identical account state. The table
235
- shows mean compute units per call (generated by `scripts/readme-bench.ts` from the benchmark run):
235
+ shows mean compute units per call (generated by `scripts/readme-bench.ts` from the benchmark run). The programs,
236
+ scenarios and harness are in [typesol/benchmarks](https://github.com/typesol/benchmarks), which pins the compiler and
237
+ toolchain and records them with every result:
236
238
 
237
239
  <!-- bench:table -->
238
240
  | Instruction | TypeSOL | Pinocchio 0.10 | Native Rust | Anchor 1.0.2 |
@@ -244,9 +246,9 @@ shows mean compute units per call (generated by `scripts/readme-bench.ts` from t
244
246
  | vault.withdraw | **315** | 370 | 1,176 | 3,628 |
245
247
  | vault.shut | **161** | 167 | 950 | 2,024 |
246
248
  | escrow.make | **3,605** | 6,179 | 9,240 | 13,304 |
247
- | escrow.take | **4,035** | 4,051 | 8,500 | 14,365 |
248
- | escrow.refund | **2,716** | 2,769 | 5,695 | 8,449 |
249
- | binary, counter / vault / escrow | 13.1 / 16.5 / 23.9 KB | 19.6 / 22.9 / 29.4 KB | 27.9 / 35.0 / 46.5 KB | 145.2 / 158.9 / 190.7 KB |
249
+ | escrow.take | **4,019** | 4,051 | 8,500 | 14,365 |
250
+ | escrow.refund | **2,696** | 2,769 | 5,695 | 8,449 |
251
+ | binary, counter / vault / escrow | 11.0 / 14.1 / 21.3 KB | 19.6 / 22.9 / 29.4 KB | 27.9 / 35.0 / 46.5 KB | 145.2 / 158.9 / 190.7 KB |
250
252
  <!-- /bench:table -->
251
253
 
252
254
  `initialize`, `open` and `make` include the PDA bump search. TypeSOL hashes PDA candidates with the sha256 and
@@ -307,7 +309,7 @@ Details are in [docs/testing.md](docs/testing.md).
307
309
 
308
310
  ## Status
309
311
 
310
- **TypeSOL is pre-1.0 (0.24).** Releases follow the organization's
312
+ **TypeSOL is pre-1.0.** Releases follow the organization's
311
313
  [versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
312
314
  change.
313
315
  - The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
package/dist/clientgen.js CHANGED
@@ -1102,7 +1102,10 @@ function generate(idl, opts) {
1102
1102
  // A seed or program path names an account: in the same group first, then at the top level.
1103
1103
  const byPath = (p, group) => flat.find((f) => `${f.group}${f.name}` === `${group}${p}`) ?? flat.find((f) => f.group === "" && f.name === p);
1104
1104
  const argType = new Map(ix.args.map((a) => [a.name, a.type]));
1105
- const seedExpr = (s, group) => {
1105
+ const seedsProp = (k) => `seeds${NAME.test(k) && !JS_RESERVED.has(k) ? `.${k}` : `[${JSON.stringify(k)}]`}`;
1106
+ const BUILDER = { arg: (n) => `args.${n}`, account: (k) => `$${k}` };
1107
+ const HELPER = { arg: (n) => seedsProp(n), account: (k) => seedsProp(k) };
1108
+ const seedExpr = (s, group, refs = BUILDER) => {
1106
1109
  if (s.kind === "const")
1107
1110
  return bytes(s.value);
1108
1111
  if (s.kind === "arg") {
@@ -1111,7 +1114,7 @@ function generate(idl, opts) {
1111
1114
  let t = argType.get(head);
1112
1115
  if (t === undefined)
1113
1116
  return { why: `the seed argument ${head} does not exist` };
1114
- let v = `args.${camel(head)}`;
1117
+ let v = refs.arg(camel(head));
1115
1118
  for (const part of rest) {
1116
1119
  const d = defs.get(definedName(t) ?? "");
1117
1120
  const f = d?.type.kind === "struct" ? (d.type.fields ?? []).find((x) => isField(x) && x.name === part) : undefined;
@@ -1134,25 +1137,30 @@ function generate(idl, opts) {
1134
1137
  if (!target)
1135
1138
  return { why: `the seed account ${s.path} is not an account of the instruction` };
1136
1139
  use("getAddressEncoder");
1137
- return `getAddressEncoder().encode(key($${target.key}))`;
1140
+ return `getAddressEncoder().encode(key(${refs.account(target.key)}))`;
1138
1141
  };
1139
- const resolvable = new Map();
1140
- for (const a of flat) {
1141
- if (!a.pda || a.address)
1142
- continue;
1143
- const seeds = a.pda.seeds.map((s) => seedExpr(s, a.group));
1144
- let program;
1142
+ /** A PDA's program as the code being written names it (undefined: this program). */
1143
+ const pdaProgram = (a, refs) => {
1145
1144
  const p = a.pda.program;
1146
1145
  if (p?.kind === "const")
1147
- program = (use("getAddressDecoder"), `programFromBytes(${bytes(p.value)})`);
1148
- else if (p?.kind === "account") {
1146
+ return (use("getAddressDecoder"), `programFromBytes(${bytes(p.value)})`);
1147
+ if (p?.kind === "account") {
1149
1148
  const target = p.path.includes(".") ? undefined : byPath(p.path, a.group);
1150
- program = target ? `key($${target.key})` : { why: `the PDA program ${p.path} is not an account of the instruction` };
1149
+ return target ? `key(${refs.account(target.key)})` : { why: `the PDA program ${p.path} is not an account of the instruction` };
1151
1150
  }
1152
- else if (p?.kind === "arg") {
1151
+ if (p?.kind === "arg") {
1153
1152
  const t = p.path.includes(".") ? undefined : argType.get(p.path);
1154
- program = t === "pubkey" || t === "publicKey" ? `args.${camel(p.path)}` : { why: "the PDA program comes from an argument that is not a Pubkey" };
1153
+ return t === "pubkey" || t === "publicKey" ? refs.arg(camel(p.path)) : { why: "the PDA program comes from an argument that is not a Pubkey" };
1155
1154
  }
1155
+ return undefined;
1156
+ };
1157
+ const resolvable = new Map();
1158
+ for (const a of flat) {
1159
+ if (!a.pda || a.address)
1160
+ continue;
1161
+ const seeds = a.pda.seeds.map((s) => seedExpr(s, a.group));
1162
+ const program = pdaProgram(a, BUILDER);
1163
+ const p = a.pda.program;
1156
1164
  const why = [...seeds, ...(program === undefined ? [] : [program])].find((x) => typeof x === "object");
1157
1165
  if (why) {
1158
1166
  notes.push(`instruction ${ix.name}: ${a.name} is not filled in (${why.why}); pass it`);
@@ -1228,11 +1236,13 @@ function generate(idl, opts) {
1228
1236
  return [k, `${k}: Address | TransactionSigner`];
1229
1237
  })).values(),
1230
1238
  ];
1231
- const rewrite = (e) => e.replace(/\bargs\.([A-Za-z0-9_$]+)/g, "seeds.$1").replace(/key\(\$([A-Za-z0-9_]+)\)/g, "key(seeds.$1)");
1239
+ // The same derivation, its inputs named as the helper's `seeds` parameter.
1240
+ const helperSeeds = a.pda.seeds.map((s) => seedExpr(s, a.group, HELPER));
1241
+ const helperProgram = pdaProgram(a, HELPER) ?? "PROGRAM_ADDRESS";
1232
1242
  use("type ProgramDerivedAddress");
1233
1243
  pdaFns.set(pn, {
1234
1244
  json,
1235
- source: ` /** The ${a.name} PDA (as ${ix.name} derives it). */\n ${prop(pn)}(seeds: { ${params.join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${rewrite(r.program ?? "PROGRAM_ADDRESS")}, seeds: [${r.seeds.map(rewrite).join(", ")}] });\n },`,
1245
+ source: ` /** The ${a.name} PDA (as ${ix.name} derives it). */\n ${prop(pn)}(seeds: { ${params.join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${helperProgram}, seeds: [${helperSeeds.join(", ")}] });\n },`,
1236
1246
  });
1237
1247
  }
1238
1248
  const metas = flat.map((a) => {
@@ -1326,6 +1336,7 @@ function generate(idl, opts) {
1326
1336
  }
1327
1337
  const bases = new Map();
1328
1338
  const seeds = [];
1339
+ const helperSeeds = []; // the same seeds, named as a `pdas` helper's `seeds` (for one seeded from its own fields)
1329
1340
  const own = [];
1330
1341
  // Inputs of a `pdas` helper for a PDA seeded from its own fields: its own field values, accounts and arguments.
1331
1342
  const params = new Map();
@@ -1334,13 +1345,14 @@ function generate(idl, opts) {
1334
1345
  let usesArgs = p?.kind === "arg";
1335
1346
  for (const sd of a.pda.seeds) {
1336
1347
  if (sd.kind === "const")
1337
- seeds.push(bytes(sd.value));
1348
+ (seeds.push(bytes(sd.value)), helperSeeds.push(bytes(sd.value)));
1338
1349
  else if (sd.kind === "arg") {
1339
1350
  const e = seedExpr(sd, a.group);
1340
1351
  if (typeof e !== "string")
1341
1352
  ok = false;
1342
1353
  else {
1343
1354
  seeds.push(e);
1355
+ helperSeeds.push(seedExpr(sd, a.group, HELPER));
1344
1356
  usesArgs = true;
1345
1357
  const head = sd.path.split(".")[0];
1346
1358
  params.set(camel(head), `${camel(head)}: ${tsType(argType.get(head), "in")}`);
@@ -1353,6 +1365,7 @@ function generate(idl, opts) {
1353
1365
  else {
1354
1366
  use("getAddressEncoder");
1355
1367
  seeds.push(`getAddressEncoder().encode(key(${acc(t)}!))`);
1368
+ helperSeeds.push(`getAddressEncoder().encode(key(${seedsProp(t.key)}))`);
1356
1369
  params.set(t.key, `${t.key}: Address | TransactionSigner`);
1357
1370
  if (auto(t))
1358
1371
  autoDep = true;
@@ -1370,6 +1383,7 @@ function generate(idl, opts) {
1370
1383
  own.push({ field: parts[1], type: field.type });
1371
1384
  params.set(camel(parts[1]), `${camel(parts[1])}: ${tsType(field.type, "in")}`);
1372
1385
  seeds.push(enc);
1386
+ helperSeeds.push(enc);
1373
1387
  }
1374
1388
  else if (!consts || auto(base))
1375
1389
  ok = false;
@@ -1399,13 +1413,11 @@ function generate(idl, opts) {
1399
1413
  continue;
1400
1414
  if (p?.kind === "arg")
1401
1415
  params.set(camel(p.path), `${camel(p.path)}: Address`);
1402
- const rewrite = (e) => e
1403
- .replace(/\bargs\.([A-Za-z0-9_$]+)/g, "seeds.$1")
1404
- .replace(/key\(ownProp\(acc, "([^"]+)"\)!\)/g, (_, k) => `key(seeds${NAME.test(k) && !JS_RESERVED.has(k) ? `.${k}` : `[${JSON.stringify(k)}]`})`);
1416
+ const helperProgram = p?.kind === "arg" ? seedsProp(camel(p.path)) : program;
1405
1417
  use("type ProgramDerivedAddress", "getProgramDerivedAddress");
1406
1418
  pdaFns.set(pn, {
1407
1419
  json,
1408
- source: ` /** The ${a.name} PDA (as ${ix.name} derives it), from the values of its own fields it is seeded with (${own.map((o) => camel(o.field)).join(", ")}). */\n ${prop(pn)}(seeds: { ${[...params.values()].join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${rewrite(program)}, seeds: [${seeds.map(rewrite).join(", ")}] });\n },`,
1420
+ source: ` /** The ${a.name} PDA (as ${ix.name} derives it), from the values of its own fields it is seeded with (${own.map((o) => camel(o.field)).join(", ")}). */\n ${prop(pn)}(seeds: { ${[...params.values()].join("; ")} }): Promise<ProgramDerivedAddress> {\n return getProgramDerivedAddress({ programAddress: ${helperProgram}, seeds: [${helperSeeds.join(", ")}] });\n },`,
1409
1421
  });
1410
1422
  continue;
1411
1423
  }
@@ -680,7 +680,7 @@ export function analyze(entry) {
680
680
  dataEnums,
681
681
  options,
682
682
  constants,
683
- events: new Set(),
683
+ events: collectEvents([...functions, ...helperNodes], structs),
684
684
  accountStructs,
685
685
  argStructs,
686
686
  fstrings,
@@ -1164,6 +1164,26 @@ function fnParamType(arrow, checker) {
1164
1164
  }
1165
1165
  return undefined;
1166
1166
  }
1167
+ /**
1168
+ * The structs a program emits (`emit<T>(…)` / `emitCpi<T>(…)` anywhere in its functions and helpers), known before
1169
+ * any code is emitted, so the IDL and the event checks do not depend on the order code is emitted in.
1170
+ */
1171
+ function collectEvents(sources, structs) {
1172
+ const events = new Set();
1173
+ for (const { node, imports } of sources) {
1174
+ const visit = (n) => {
1175
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
1176
+ const name = imports.get(n.expression.text);
1177
+ const t = n.typeArguments?.[0];
1178
+ if ((name === "emit" || name === "emitCpi") && t && ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && structs.has(t.typeName.text))
1179
+ events.add(t.typeName.text);
1180
+ }
1181
+ ts.forEachChild(n, visit);
1182
+ };
1183
+ visit(node);
1184
+ }
1185
+ return events;
1186
+ }
1167
1187
  /** Whether a function returns a function: typed so, or its body is (or only returns) an arrow. */
1168
1188
  function returnsFunction(fn) {
1169
1189
  if (fn.type)
@@ -24,6 +24,16 @@ export declare abstract class EmitterBase {
24
24
  protected readonly pendingInit: Set<string>;
25
25
  /** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
26
26
  protected readonly bumps: Map<string, string>;
27
+ /**
28
+ * Seed arrays already declared (no spare slot, every seed read in place): the C entries, and where the array is.
29
+ * An identical list in the same or an inner block reuses it (see seeds()).
30
+ */
31
+ protected readonly seedArrays: Map<string, {
32
+ name: string;
33
+ scope: Map<string, Local>;
34
+ at: number;
35
+ text: string;
36
+ }>;
27
37
  /** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
28
38
  protected inlineFrames: {
29
39
  label: string;
@@ -23,6 +23,11 @@ export class EmitterBase {
23
23
  pendingInit = new Set();
24
24
  /** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
25
25
  bumps = new Map();
26
+ /**
27
+ * Seed arrays already declared (no spare slot, every seed read in place): the C entries, and where the array is.
28
+ * An identical list in the same or an inner block reuses it (see seeds()).
29
+ */
30
+ seedArrays = new Map();
26
31
  /** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
27
32
  inlineFrames = [];
28
33
  /** Closures being expanded (a closure calling itself cannot be expanded). */
@@ -300,6 +305,7 @@ export class EmitterBase {
300
305
  created: new Set(this.created),
301
306
  pendingInit: new Set(this.pendingInit),
302
307
  bumps: new Map(this.bumps),
308
+ seedArrays: new Map(this.seedArrays),
303
309
  };
304
310
  try {
305
311
  return this.reporter.guard(fn);
@@ -322,6 +328,8 @@ export class EmitterBase {
322
328
  }
323
329
  this.bumps.clear();
324
330
  saved.bumps.forEach((v, k) => this.bumps.set(k, v));
331
+ this.seedArrays.clear();
332
+ saved.seedArrays.forEach((v, k) => this.seedArrays.set(k, v));
325
333
  }
326
334
  }
327
335
  fail(node, message) {
@@ -103,8 +103,17 @@ export declare abstract class CpiEmitter extends CallEmitter {
103
103
  protected corePlugin(n: ts.Expression, withAuthority: boolean): MplField[];
104
104
  /** CreateV1 / CreateCollectionV1 plugins: Option<Vec<PluginAuthorityPair>>. */
105
105
  protected corePluginList(n: ts.Expression | undefined): MplField[];
106
- /** Metaplex Core: assets and collections (with plugins), plugins added, updated and removed, transfer and burn. */
106
+ /**
107
+ * Metaplex Core: assets and collections (with plugins), plugins and external plugin adapters added, updated, written
108
+ * and removed, transfer and burn. Each method is a row of CORE_OPS below: its accounts in order, its data.
109
+ */
107
110
  protected coreOp(node: ts.CallExpression, core: AccountParam, method: string): undefined;
111
+ /**
112
+ * Core instructions: `args` fixed arguments (then an optional plugin list when `plugins`, then optional signer
113
+ * seeds), accounts in order (`[argument, label, writable, signer]`, or "self": the Core program, Core's "none" for an
114
+ * optional account), at most `max` bytes of data.
115
+ */
116
+ private coreOps;
108
117
  /** The fields of a Core object literal (`{ kind: "AppData", … }`) by name. */
109
118
  private coreProps;
110
119
  /** PluginAuthority: "none" | "owner" | "updateAuthority" (literals), or an address. */
@@ -207,6 +207,7 @@ export class CpiEmitter extends CallEmitter {
207
207
  ts.isPostfixUnaryExpression(n) ||
208
208
  ts.forEachChild(n, mutates) === true;
209
209
  const inPlace = !node.elements.some(mutates);
210
+ const mark = this.lines.length;
210
211
  for (const el of node.elements) {
211
212
  const desc = this.describeSeed(el);
212
213
  if (!desc)
@@ -252,9 +253,19 @@ export class CpiEmitter extends CallEmitter {
252
253
  else
253
254
  this.fail(el, "seeds can be strings, Pubkeys, integers or unit enum values");
254
255
  }
256
+ // The same list again (escrow: the vault transfer, then its close) reuses the array declared for the first, when
257
+ // nothing was copied into temporaries (every entry points at a literal or at the place itself, so the array reads
258
+ // what a new one would), it has no spare slot (callers never write into those arrays) and it is still in scope.
259
+ const key = parts.join(", ");
260
+ const reusable = extra === 0 && parts.length > 0 && this.lines.length === mark;
261
+ const known = reusable ? this.seedArrays.get(key) : undefined;
262
+ if (known && this.scopes.includes(known.scope) && this.lines[known.at]?.trim() === known.text)
263
+ return { name: known.name, count: parts.length, idl };
255
264
  const name = this.tmp("seeds");
256
265
  const slots = parts.length + extra;
257
266
  this.line(`SolSignerSeed ${name}[${Math.max(slots, 1)}] = {${parts.join(", ")}};`);
267
+ if (reusable)
268
+ this.seedArrays.set(key, { name, scope: this.scopes[this.scopes.length - 1], at: this.lines.length - 1, text: this.lines.at(-1).trim() });
258
269
  return { name, count: parts.length, idl };
259
270
  }
260
271
  describeSeed(el) {
@@ -1715,179 +1726,180 @@ export class CpiEmitter extends CallEmitter {
1715
1726
  });
1716
1727
  return [{ bytes: [1, els.length & 0xff, 0, 0, 0] }, ...els.flatMap((e) => this.corePlugin(e, true))];
1717
1728
  }
1718
- /** Metaplex Core: assets and collections (with plugins), plugins added, updated and removed, transfer and burn. */
1729
+ /**
1730
+ * Metaplex Core: assets and collections (with plugins), plugins and external plugin adapters added, updated, written
1731
+ * and removed, transfer and burn. Each method is a row of CORE_OPS below: its accounts in order, its data.
1732
+ */
1719
1733
  coreOp(node, core, method) {
1720
1734
  const a = node.arguments;
1721
1735
  const p = `a_${core.name}`;
1722
- const seedsAt = (i) => (a[i] ? this.signingSeeds(a[i]) : { name: "NULL", count: 0 });
1723
- const str = (n) => ({ value: n, type: STRING });
1724
- // An array literal of object literals after the uri is the plugin list; the signer seeds come after it.
1736
+ const op = this.coreOps()[method];
1737
+ if (!op)
1738
+ this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, addPlugin, updatePlugin, removePlugin, addExternalPlugin, removeExternalPlugin, writeExternalPluginData (and the Collection forms), transferAsset and burnAsset, not ${method}`);
1739
+ // An array literal of object literals right after the fixed arguments is the plugin list; the signer seeds follow.
1725
1740
  const isPlugins = (n) => !!n && ts.isArrayLiteralExpression(n) && (n.elements.length === 0 || n.elements.every((e) => ts.isObjectLiteralExpression(e)));
1726
- const self = [p, false, false];
1727
- switch (method) {
1728
- case "createAsset": {
1729
- // CreateV1: asset, collection, authority, payer, owner, updateAuthority, systemProgram, logWrapper.
1730
- const plugins = isPlugins(a[8]) ? a[8] : undefined;
1731
- if (a.length < 8 || a.length > (plugins ? 10 : 9))
1732
- this.fail(node, "createAsset(asset, collection | null, authority | null, payer, owner | null, systemProgram, name, uri, plugins?, signerSeeds?)");
1733
- const metas = [
1734
- this.mplMeta(a[0], core, "the asset", true, true),
1735
- this.mplMeta(a[1], core, "the collection", true, false),
1736
- this.mplMeta(a[2], core, "the collection authority", false, true),
1737
- this.mplMeta(a[3], core, "the payer", true, true),
1738
- this.mplMeta(a[4], core, "the owner", false, false),
1739
- [p, false, false], // update authority: the collection's, or the payer by default
1740
- this.mplMeta(a[5], core, "the system program", false, false),
1741
- [p, false, false],
1742
- ];
1743
- const data = this.mplData(node, 2048, [{ bytes: [0, 0] }, str(a[6]), str(a[7]), ...this.corePluginList(plugins)]);
1744
- this.cpi(p, metas, data.buf, data.len, seedsAt(plugins ? 9 : 8));
1745
- return undefined;
1746
- }
1747
- case "createCollection": {
1741
+ const list = op.plugins && isPlugins(a[op.args]) ? a[op.args] : undefined;
1742
+ const seedsAt = op.args + (list ? 1 : 0);
1743
+ if (op.plugins ? a.length < op.args || a.length > seedsAt + 1 : a.length !== op.args && a.length !== op.args + 1)
1744
+ this.fail(node, op.usage(method));
1745
+ const metas = op.metas.map((m) => (m === "self" ? [p, false, false] : this.mplMeta(a[m[0]], core, m[1], m[2], m[3])));
1746
+ const data = this.mplData(node, op.max, op.data(a, list));
1747
+ this.cpi(p, metas, data.buf, data.len, a[seedsAt] ? this.signingSeeds(a[seedsAt]) : { name: "NULL", count: 0 });
1748
+ return undefined;
1749
+ }
1750
+ /**
1751
+ * Core instructions: `args` fixed arguments (then an optional plugin list when `plugins`, then optional signer
1752
+ * seeds), accounts in order (`[argument, label, writable, signer]`, or "self": the Core program, Core's "none" for an
1753
+ * optional account), at most `max` bytes of data.
1754
+ */
1755
+ coreOps() {
1756
+ const str = (n) => ({ value: n, type: STRING });
1757
+ const asset = [0, "the asset", true, false];
1758
+ const assetPlugin = {
1759
+ usage: (m) => `${m}(asset, collection | null, payer, authority | null, systemProgram, ${m === "removePlugin" ? "kind" : "plugin"}, signerSeeds?)`,
1760
+ args: 6,
1761
+ // Add/Remove/UpdatePluginV1: asset, collection, payer, authority, systemProgram, logWrapper.
1762
+ metas: [
1763
+ asset,
1764
+ [1, "the collection", true, false],
1765
+ [2, "the payer", true, true],
1766
+ [3, "the authority", false, true],
1767
+ [4, "the system program", false, false],
1768
+ "self",
1769
+ ],
1770
+ max: 2048,
1771
+ };
1772
+ const collectionPlugin = {
1773
+ usage: (m) => `${m}(collection, payer, authority | null, systemProgram, ${m === "removeCollectionPlugin" ? "kind" : "plugin"}, signerSeeds?)`,
1774
+ args: 5,
1775
+ // Add/Remove/UpdateCollectionPluginV1: collection, payer, authority, systemProgram, logWrapper.
1776
+ metas: [
1777
+ [0, "the collection", true, false],
1778
+ [1, "the payer", true, true],
1779
+ [2, "the authority", false, true],
1780
+ [3, "the system program", false, false],
1781
+ "self",
1782
+ ],
1783
+ max: 2048,
1784
+ };
1785
+ const assetPluginOp = (method, disc) => ({
1786
+ ...assetPlugin,
1787
+ data: (a) => [
1788
+ { bytes: [disc] },
1789
+ ...(method === "removePlugin" ? [this.corePluginType(a[5])] : this.corePlugin(a[5], method === "addPlugin")),
1790
+ ],
1791
+ });
1792
+ const collectionPluginOp = (method, disc) => ({
1793
+ ...collectionPlugin,
1794
+ data: (a) => [
1795
+ { bytes: [disc] },
1796
+ ...(method === "removeCollectionPlugin" ? [this.corePluginType(a[4])] : this.corePlugin(a[4], method === "addCollectionPlugin")),
1797
+ ],
1798
+ });
1799
+ // Add/RemoveExternalPluginAdapterV1: asset, collection, payer, authority, systemProgram, logWrapper; the write has a
1800
+ // buffer account (left out: the data is in the instruction) before systemProgram. The Collection forms drop the asset.
1801
+ const external = (method, disc, collection) => {
1802
+ const write = method.startsWith("write");
1803
+ const add = method.startsWith("add");
1804
+ const n = (collection ? 5 : 6) + (write ? 1 : 0);
1805
+ const at = collection ? 4 : 5;
1806
+ const head = collection ? "collection, payer, authority | null, systemProgram" : "asset, collection | null, payer, authority | null, systemProgram";
1807
+ const metas = collection
1808
+ ? [
1809
+ [0, "the collection", true, false],
1810
+ [1, "the payer", true, true],
1811
+ [2, "the authority", false, true],
1812
+ ]
1813
+ : [asset, [1, "the collection", true, false], [2, "the payer", true, true], [3, "the authority", false, true]];
1814
+ if (write)
1815
+ metas.push("self");
1816
+ metas.push([collection ? 3 : 4, "the system program", false, false], "self");
1817
+ return {
1818
+ usage: (m) => `${m}(${head}, ${add ? "plugin" : write ? "key, data" : "key"}, signerSeeds?)`,
1819
+ args: n,
1820
+ metas,
1821
+ max: 1024,
1822
+ data: (a) => [
1823
+ { bytes: [disc] },
1824
+ ...(add
1825
+ ? this.coreExternalPlugin(a[at])
1826
+ : write
1827
+ ? [...this.coreExternalKey(a[at]), ...this.coreExternalData(a[at + 1])]
1828
+ : this.coreExternalKey(a[at])),
1829
+ ],
1830
+ };
1831
+ };
1832
+ return {
1833
+ createAsset: {
1834
+ usage: () => "createAsset(asset, collection | null, authority | null, payer, owner | null, systemProgram, name, uri, plugins?, signerSeeds?)",
1835
+ args: 8,
1836
+ plugins: true,
1837
+ // CreateV1: asset, collection, authority, payer, owner, updateAuthority (the collection's, or the payer by
1838
+ // default), systemProgram, logWrapper.
1839
+ metas: [
1840
+ [0, "the asset", true, true],
1841
+ [1, "the collection", true, false],
1842
+ [2, "the collection authority", false, true],
1843
+ [3, "the payer", true, true],
1844
+ [4, "the owner", false, false],
1845
+ "self",
1846
+ [5, "the system program", false, false],
1847
+ "self",
1848
+ ],
1849
+ max: 2048,
1850
+ data: (a, plugins) => [{ bytes: [0, 0] }, str(a[6]), str(a[7]), ...this.corePluginList(plugins)],
1851
+ },
1852
+ createCollection: {
1853
+ usage: () => "createCollection(collection, updateAuthority | null, payer, systemProgram, name, uri, plugins?, signerSeeds?)",
1854
+ args: 6,
1855
+ plugins: true,
1748
1856
  // CreateCollectionV1: collection, updateAuthority, payer, systemProgram.
1749
- const plugins = isPlugins(a[6]) ? a[6] : undefined;
1750
- if (a.length < 6 || a.length > (plugins ? 8 : 7))
1751
- this.fail(node, "createCollection(collection, updateAuthority | null, payer, systemProgram, name, uri, plugins?, signerSeeds?)");
1752
- const metas = [
1753
- this.mplMeta(a[0], core, "the collection", true, true),
1754
- this.mplMeta(a[1], core, "the update authority", false, false),
1755
- this.mplMeta(a[2], core, "the payer", true, true),
1756
- this.mplMeta(a[3], core, "the system program", false, false),
1757
- ];
1758
- const data = this.mplData(node, 2048, [{ bytes: [1] }, str(a[4]), str(a[5]), ...this.corePluginList(plugins)]);
1759
- this.cpi(p, metas, data.buf, data.len, seedsAt(plugins ? 7 : 6));
1760
- return undefined;
1761
- }
1762
- case "addPlugin":
1763
- case "updatePlugin":
1764
- case "removePlugin": {
1765
- // Add/Remove/UpdatePluginV1: asset, collection, payer, authority, systemProgram, logWrapper.
1766
- const last = method === "removePlugin" ? "kind" : "plugin";
1767
- if (a.length !== 6 && a.length !== 7)
1768
- this.fail(node, `${method}(asset, collection | null, payer, authority | null, systemProgram, ${last}, signerSeeds?)`);
1769
- const metas = [
1770
- this.mplMeta(a[0], core, "the asset", true, false),
1771
- this.mplMeta(a[1], core, "the collection", true, false),
1772
- this.mplMeta(a[2], core, "the payer", true, true),
1773
- this.mplMeta(a[3], core, "the authority", false, true),
1774
- this.mplMeta(a[4], core, "the system program", false, false),
1775
- self,
1776
- ];
1777
- const body = method === "removePlugin" ? [this.corePluginType(a[5])] : this.corePlugin(a[5], method === "addPlugin");
1778
- const data = this.mplData(node, 2048, [{ bytes: [{ addPlugin: 2, removePlugin: 4, updatePlugin: 6 }[method]] }, ...body]);
1779
- this.cpi(p, metas, data.buf, data.len, seedsAt(6));
1780
- return undefined;
1781
- }
1782
- case "addCollectionPlugin":
1783
- case "updateCollectionPlugin":
1784
- case "removeCollectionPlugin": {
1785
- // Add/Remove/UpdateCollectionPluginV1: collection, payer, authority, systemProgram, logWrapper.
1786
- const last = method === "removeCollectionPlugin" ? "kind" : "plugin";
1787
- if (a.length !== 5 && a.length !== 6)
1788
- this.fail(node, `${method}(collection, payer, authority | null, systemProgram, ${last}, signerSeeds?)`);
1789
- const metas = [
1790
- this.mplMeta(a[0], core, "the collection", true, false),
1791
- this.mplMeta(a[1], core, "the payer", true, true),
1792
- this.mplMeta(a[2], core, "the authority", false, true),
1793
- this.mplMeta(a[3], core, "the system program", false, false),
1794
- self,
1795
- ];
1796
- const body = method === "removeCollectionPlugin" ? [this.corePluginType(a[4])] : this.corePlugin(a[4], method === "addCollectionPlugin");
1797
- const data = this.mplData(node, 2048, [{ bytes: [{ addCollectionPlugin: 3, removeCollectionPlugin: 5, updateCollectionPlugin: 7 }[method]] }, ...body]);
1798
- this.cpi(p, metas, data.buf, data.len, seedsAt(5));
1799
- return undefined;
1800
- }
1801
- case "transferAsset": {
1857
+ metas: [
1858
+ [0, "the collection", true, true],
1859
+ [1, "the update authority", false, false],
1860
+ [2, "the payer", true, true],
1861
+ [3, "the system program", false, false],
1862
+ ],
1863
+ max: 2048,
1864
+ data: (a, plugins) => [{ bytes: [1] }, str(a[4]), str(a[5]), ...this.corePluginList(plugins)],
1865
+ },
1866
+ addPlugin: assetPluginOp("addPlugin", 2),
1867
+ removePlugin: assetPluginOp("removePlugin", 4),
1868
+ updatePlugin: assetPluginOp("updatePlugin", 6),
1869
+ addCollectionPlugin: collectionPluginOp("addCollectionPlugin", 3),
1870
+ removeCollectionPlugin: collectionPluginOp("removeCollectionPlugin", 5),
1871
+ updateCollectionPlugin: collectionPluginOp("updateCollectionPlugin", 7),
1872
+ transferAsset: {
1873
+ usage: () => "transferAsset(asset, collection | null, payer, authority | null, newOwner, signerSeeds?)",
1874
+ args: 5,
1802
1875
  // TransferV1: asset, collection, payer, authority, newOwner, systemProgram, logWrapper.
1803
- if (a.length !== 5 && a.length !== 6)
1804
- this.fail(node, "transferAsset(asset, collection | null, payer, authority | null, newOwner, signerSeeds?)");
1805
- const metas = [
1806
- this.mplMeta(a[0], core, "the asset", true, false),
1807
- this.mplMeta(a[1], core, "the collection", false, false),
1808
- this.mplMeta(a[2], core, "the payer", true, true),
1809
- this.mplMeta(a[3], core, "the authority", false, true),
1810
- this.mplMeta(a[4], core, "the new owner", false, false),
1811
- [p, false, false],
1812
- [p, false, false],
1813
- ];
1814
- const data = this.mplData(node, 8, [{ bytes: [14, 0] }]);
1815
- this.cpi(p, metas, data.buf, data.len, seedsAt(5));
1816
- return undefined;
1817
- }
1818
- case "burnAsset": {
1876
+ metas: [
1877
+ asset,
1878
+ [1, "the collection", false, false],
1879
+ [2, "the payer", true, true],
1880
+ [3, "the authority", false, true],
1881
+ [4, "the new owner", false, false],
1882
+ "self",
1883
+ "self",
1884
+ ],
1885
+ max: 8,
1886
+ data: () => [{ bytes: [14, 0] }],
1887
+ },
1888
+ burnAsset: {
1889
+ usage: () => "burnAsset(asset, collection | null, payer, authority | null, signerSeeds?)",
1890
+ args: 4,
1819
1891
  // BurnV1: asset, collection, payer, authority, systemProgram, logWrapper.
1820
- if (a.length !== 4 && a.length !== 5)
1821
- this.fail(node, "burnAsset(asset, collection | null, payer, authority | null, signerSeeds?)");
1822
- const metas = [
1823
- this.mplMeta(a[0], core, "the asset", true, false),
1824
- this.mplMeta(a[1], core, "the collection", true, false),
1825
- this.mplMeta(a[2], core, "the payer", true, true),
1826
- this.mplMeta(a[3], core, "the authority", false, true),
1827
- [p, false, false],
1828
- [p, false, false],
1829
- ];
1830
- const data = this.mplData(node, 8, [{ bytes: [12, 0] }]);
1831
- this.cpi(p, metas, data.buf, data.len, seedsAt(4));
1832
- return undefined;
1833
- }
1834
- case "addExternalPlugin":
1835
- case "removeExternalPlugin":
1836
- case "writeExternalPluginData": {
1837
- // Add/RemoveExternalPluginAdapterV1: asset, collection, payer, authority, systemProgram, logWrapper;
1838
- // WriteExternalPluginAdapterDataV1 has a buffer account (left out: the data is in the instruction) before systemProgram.
1839
- const write = method === "writeExternalPluginData";
1840
- const n = write ? 7 : 6;
1841
- if (a.length !== n && a.length !== n + 1)
1842
- this.fail(node, `${method}(asset, collection | null, payer, authority | null, systemProgram, ${method === "addExternalPlugin" ? "plugin" : write ? "key, data" : "key"}, signerSeeds?)`);
1843
- const metas = [
1844
- this.mplMeta(a[0], core, "the asset", true, false),
1845
- this.mplMeta(a[1], core, "the collection", true, false),
1846
- this.mplMeta(a[2], core, "the payer", true, true),
1847
- this.mplMeta(a[3], core, "the authority", false, true),
1848
- ...(write ? [self] : []),
1849
- this.mplMeta(a[4], core, "the system program", false, false),
1850
- self,
1851
- ];
1852
- const body = method === "addExternalPlugin"
1853
- ? this.coreExternalPlugin(a[5])
1854
- : write
1855
- ? [...this.coreExternalKey(a[5]), ...this.coreExternalData(a[6])]
1856
- : this.coreExternalKey(a[5]);
1857
- const data = this.mplData(node, 1024, [{ bytes: [{ addExternalPlugin: 22, removeExternalPlugin: 24, writeExternalPluginData: 28 }[method]] }, ...body]);
1858
- this.cpi(p, metas, data.buf, data.len, seedsAt(n));
1859
- return undefined;
1860
- }
1861
- case "addCollectionExternalPlugin":
1862
- case "removeCollectionExternalPlugin":
1863
- case "writeCollectionExternalPluginData": {
1864
- // The same on a collection: collection, payer, authority, (buffer,) systemProgram, logWrapper.
1865
- const write = method === "writeCollectionExternalPluginData";
1866
- const n = write ? 6 : 5;
1867
- if (a.length !== n && a.length !== n + 1)
1868
- this.fail(node, `${method}(collection, payer, authority | null, systemProgram, ${method === "addCollectionExternalPlugin" ? "plugin" : write ? "key, data" : "key"}, signerSeeds?)`);
1869
- const metas = [
1870
- this.mplMeta(a[0], core, "the collection", true, false),
1871
- this.mplMeta(a[1], core, "the payer", true, true),
1872
- this.mplMeta(a[2], core, "the authority", false, true),
1873
- ...(write ? [self] : []),
1874
- this.mplMeta(a[3], core, "the system program", false, false),
1875
- self,
1876
- ];
1877
- const body = method === "addCollectionExternalPlugin"
1878
- ? this.coreExternalPlugin(a[4])
1879
- : write
1880
- ? [...this.coreExternalKey(a[4]), ...this.coreExternalData(a[5])]
1881
- : this.coreExternalKey(a[4]);
1882
- const data = this.mplData(node, 1024, [
1883
- { bytes: [{ addCollectionExternalPlugin: 23, removeCollectionExternalPlugin: 25, writeCollectionExternalPluginData: 29 }[method]] },
1884
- ...body,
1885
- ]);
1886
- this.cpi(p, metas, data.buf, data.len, seedsAt(n));
1887
- return undefined;
1888
- }
1889
- }
1890
- this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, addPlugin, updatePlugin, removePlugin, addExternalPlugin, removeExternalPlugin, writeExternalPluginData (and the Collection forms), transferAsset and burnAsset, not ${method}`);
1892
+ metas: [asset, [1, "the collection", true, false], [2, "the payer", true, true], [3, "the authority", false, true], "self", "self"],
1893
+ max: 8,
1894
+ data: () => [{ bytes: [12, 0] }],
1895
+ },
1896
+ addExternalPlugin: external("addExternalPlugin", 22, false),
1897
+ removeExternalPlugin: external("removeExternalPlugin", 24, false),
1898
+ writeExternalPluginData: external("writeExternalPluginData", 28, false),
1899
+ addCollectionExternalPlugin: external("addCollectionExternalPlugin", 23, true),
1900
+ removeCollectionExternalPlugin: external("removeCollectionExternalPlugin", 25, true),
1901
+ writeCollectionExternalPluginData: external("writeCollectionExternalPluginData", 29, true),
1902
+ };
1891
1903
  }
1892
1904
  /** The fields of a Core object literal (`{ kind: "AppData", … }`) by name. */
1893
1905
  coreProps(node, what) {
@@ -22,6 +22,21 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
22
22
  `compiler/index.ts` wires the stages together. `generate()` runs analyze, emit and IDL generation in memory, and
23
23
  `build()` also compiles and writes the output files.
24
24
 
25
+ ## Repository layout
26
+
27
+ | Path | Contents |
28
+ | --- | --- |
29
+ | `src/` | The compiler (`src/compiler/`) and the tools around it: CLI, workspace, client, CPI and port generators, testing, IDL publishing (source map below) |
30
+ | `runtime/` | `typesol_rt.h`, the C runtime every program includes ([runtime.md](runtime.md)) |
31
+ | `lib/` | `typesol.d.ts`, the language surface programs typecheck against |
32
+ | `ts-plugin/` | The editor plugin (`typesol/ts-plugin`) |
33
+ | `examples/` | Example programs; the `typesol init` templates are copies of three of them |
34
+ | `test/` | The test suite: `fixtures/` (programs the tests compile), `ports/` (TypeSOL rewrites of solana-developers/program-examples), `golden/` (pinned generated clients), `programs/` (third-party program binaries, see its SOURCES.md) |
35
+ | `bench/anchor/` | Anchor twins of the counter, vault and escrow examples, for the differential tests (`pnpm bench:anchor`) and the zero-copy client test; the benchmarks themselves are in [typesol/benchmarks](https://github.com/typesol/benchmarks) |
36
+ | `scripts/` | `smoke.ts` and `localnet.sh` (a local validator run), `fetch-programs.sh` (third-party binaries), `readme-bench.ts` (the README numbers) |
37
+ | `docs/` | User and contributor documentation (also published on typesol.com) |
38
+ | `assets/` | The CLI's logo and the README images (`readme/`, generated) |
39
+
25
40
  ## Source map
26
41
 
27
42
  | Path | Role |
@@ -34,11 +49,17 @@ program.so + program.build.json (manifest: versions, sha256 of sources and binar
34
49
  | `src/compiler/idl.ts` | Turns the model into an Anchor IDL: accounts with `pda`, `relations`, `address` and `optional`, plus types, events, errors and docs. |
35
50
  | `src/compiler/toolchain.ts` | Finds the Agave SDK and runs clang and lld with the SDK's flags, plus per-function sections, `--gc-sections` and `--strip-all` so unused code and symbol tables stay out of the binary. |
36
51
  | `src/compiler/diagnostics.ts` | `Reporter` and `CompileError`: diagnostics with file, line and column. |
52
+ | `src/compiler/anchor-errors.ts` | Anchor's own error codes (`AnchorError.X`), shared by `require` and the generated clients. |
53
+ | `src/compiler/index.ts` | The compiler's API (`generate`, `build`, `verifyBinary`, the build manifest). |
37
54
  | `runtime/typesol_rt.h` | The C runtime: error codes, word-wise memory helpers, the entry point's input parser (`ts_deserialize`), account checks, checked math, Borsh helpers, CPIs (System, Token, Token-2022, ATA, Metaplex), oracles and the heap allocator. Header-only and `static inline`, so unused code is dropped. |
38
55
  | `src/workspace.ts` | `typesol.json` projects, program keypairs, `typesol init` templates and deploy arguments. |
39
56
  | `src/cpigen.ts` | `typesol cpi`: reads an Anchor IDL and writes typed TypeSOL CPI helpers. |
40
57
  | `src/clientgen.ts` | The TypeScript client (`<name>.client.ts`, [client.md](client.md)): reads an IDL, validates it (it may come from anywhere), and writes one `@solana/kit` file. Not part of the compiler core: `workspace.ts` and the CLI write it after `build()`, and `typesol client` writes it for any IDL. |
41
- | `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program. |
58
+ | `src/idlpublish.ts` | `typesol idl publish` through the Program Metadata program, and reading a program's IDL back from the chain (Program Metadata or Anchor's IDL account) for `typesol client --address`. |
59
+ | `src/portgen.ts` | `typesol port`: a TypeSOL program skeleton from an Anchor IDL (structs, enums, errors, events, one function per instruction). |
60
+ | `src/templates.ts` | The programs and tests `typesol init --template` writes (copies of `examples/`; a test checks they match). |
61
+ | `src/svmrpc.ts` | `svmConnection`: a kit RPC and subscriptions pair answered by LiteSVM, so generated clients run in tests (exported by `typesol/testing`). |
62
+ | `src/keys.ts` | Base58 and program keypair helpers. |
42
63
  | `ts-plugin/index.js` | `typesol/ts-plugin` (CommonJS, loaded by tsserver): runs `typesol check --json --stdin` on program entries and adds the diagnostics. |
43
64
  | `src/testing.ts` | `typesol/testing`: LiteSVM test helpers for projects (accounts by name from the IDL, plain values). |
44
65
  | `src/onchain.ts` | `typesol verify --address`: a deployed program's ELF, authority and deploy slot under loader v2, v3 or v4. |
@@ -54,7 +75,7 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
54
75
 
55
76
  | File | Layer | Contents |
56
77
  | --- | --- | --- |
57
- | `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`) |
78
+ | `shared.ts` | | `Value`, `Ref` and `Local`, limits, and the C type helpers (`valueC`, `fieldC`, `storageDecl`, `cString`), growable Vec placement (`vecPlace`, `vecLenCheck`) |
58
79
  | `base.ts` | `EmitterBase` | function state; `line()`, `tmp()`, `settle()`, scopes, `fail()`, account lookup |
59
80
  | `storage.ts` | `StorageEmitter` | references into account data and locals, reads and writes, indexing, aliasing, argument validation |
60
81
  | `expressions.ts` | `ExpressionEmitter` | literals, operators and checked arithmetic, property reads, conditionals, text, casts |
@@ -63,6 +84,9 @@ call evaluates expressions), the method is declared `abstract` at the end of `ba
63
84
  | `statements.ts` | `StatementEmitter` | variables, assignment, `if`/loops/`switch`, `return` |
64
85
  | `function.ts` | `FunctionEmitter` | the instruction prologue (account checks, constraints, argument decoding) and epilogue |
65
86
 
87
+ `borsh.ts` writes the Borsh pack/unpack codecs of `@layout borsh` structs, and `program.ts` assembles the C file
88
+ (typedefs, codecs, helpers, instructions, the entry point). `index.ts` is the emitter's export.
89
+
66
90
  ## Rules the emitter keeps
67
91
 
68
92
  These invariants are easy to break. Each one has tests.
@@ -0,0 +1,59 @@
1
+ # The C runtime
2
+
3
+ `runtime/typesol_rt.h` is included by every generated program. It holds what the emitted C calls: error codes, the
4
+ entry point's input parser, account checks, checked arithmetic, Borsh helpers, CPIs, sysvars, strings and the heap.
5
+ This page is for people changing it; programs never call it directly.
6
+
7
+ ## Conventions
8
+
9
+ - **Header-only, `static inline`.** Each program compiles the header with its own code, and `--gc-sections` drops
10
+ what it does not use. `always_inline` marks the few helpers that must disappear into their caller (word loads and
11
+ stores, `ts_memcpy`, checked add/subtract); `noinline` marks the ones that should exist once per program because
12
+ many call sites would otherwise each grow the binary (`ts_fail`, the rent fallback `ts_rent_slow`, PDA search).
13
+ - **Errors are return values.** A function that can fail returns `uint64_t`: `SUCCESS`, or an error code. Fail with
14
+ `TS_FAIL(code, "TypeSOL: …")` (logs the message, then returns the code) and propagate with `TS_TRY(call)`.
15
+ Nothing aborts: the instruction returns the code, and the runtime reports it as `Custom(code)`.
16
+ - **Error codes follow Anchor.** Where Anchor has a framework error with the same meaning, its number is used (2006
17
+ ConstraintSeeds, 3003 AccountDidNotDeserialize, …), so Anchor and Codama clients name failures correctly.
18
+ TypeSOL-only conditions are 5100–5199; user errors start at 6000. Every code is listed in [errors.md](errors.md).
19
+ - **No byte loops from the SDK.** The SDK's `sol_memcpy`, `sol_memcmp` and `SolPubkey_same` move one byte per
20
+ iteration. The runtime uses `ts_ld64` / `ts_st64` (unaligned 8-byte loads and stores, which SBF allows) in
21
+ `ts_memcpy`, `ts_memzero`, `ts_mem_eq`, `ts_key_eq` and `ts_memmove`.
22
+ - **Same results as the reference.** Fast paths must give bit-identical results to what they replace: `ts_add_overflow_u64`
23
+ matches `__builtin_add_overflow`, the rent fast path and `ts_rent_slow` match the double computation, PDA search
24
+ matches `sol_try_find_program_address`. A change here needs a test against the reference, not only against itself.
25
+
26
+ ## Sections
27
+
28
+ In file order:
29
+
30
+ | Section | What it provides |
31
+ | --- | --- |
32
+ | Error codes, `TS_FAIL`, `TS_TRY` | The codes above and the two macros |
33
+ | Word-wise memory helpers | `ts_ld64`, `ts_st64`, `ts_memcpy`, `ts_memmove`, `ts_memzero`, `ts_mem_eq`; checked u64 add/subtract/multiply |
34
+ | Well-known program ids | System, Token, Token-2022, ATA, Metaplex and the other programs the language names |
35
+ | Strings | `TS_Str` views (pointer, length), UTF-8 validation, comparison, `FixedString<N>` storage |
36
+ | Entry point input | `TsAccount` (the loader's serialized account, read in place), `TsParams`, `ts_deserialize` |
37
+ | Account checks | owner, discriminator, signer, writable, executable, address, size, uniqueness (`ts_check_*`), growable Vec lengths |
38
+ | Little-endian IO | `ts_read_le` and friends for argument decoding and Borsh |
39
+ | Sysvars | clock, rent (`ts_rent`, `ts_minimum_balance_of`, `ts_minimum_balance`), program data |
40
+ | PDA | `ts_pda_hash`, `ts_create_pda_address`, `ts_find_pda` (canonical bump), `ts_require_pda` |
41
+ | System program CPIs | create, assign, allocate, transfer, `ts_create_pda`, `ts_resize` / `ts_realloc`, close |
42
+ | Token-2022 extensions | TLV walking for the extensions the language reads |
43
+ | Metaplex Core and Bubblegum | account readers (`coreAssetOwner`, …) |
44
+ | SPL Token / Token-2022 CPIs | transfer, mint, burn, approve, close, freeze and their checked forms |
45
+ | Return data | `sol_set_return_data` wrappers |
46
+ | Crypto | sha256, keccak256, ed25519 / secp256k1 verification helpers, Merkle proofs |
47
+ | Heap | `ts_alloc`, a bump allocator (32 KiB by default, `HEAP_SIZE_KIB` up to 256), string building (`ts_str_join`, `ts_str_append`, number and base58 text) |
48
+ | Oracles | Pyth price updates, Switchboard |
49
+ | Associated token accounts, token metadata, transfer hooks | ATA derivation and creation, Token Metadata CPIs, transfer-hook extra accounts |
50
+
51
+ ## Changing it
52
+
53
+ - Every runtime change can move compute units or binary size. Measure it before and after with
54
+ [typesol/benchmarks](https://github.com/typesol/benchmarks) pointed at your checkout, and put the difference in
55
+ the pull request (see CONTRIBUTING).
56
+ - A change that alters behavior (a result, an error code, a log line, account bytes) is a language change and goes
57
+ in the CHANGELOG as one. One that only changes cost says so: "same results".
58
+ - New error codes: add the `#define` here, the row in [errors.md](errors.md) and the name in the compiler's runtime
59
+ error list (`TYPESOL_ERRORS` in `src/clientgen.ts`), so clients name it (a test checks every code has a name).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typesol/cli",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "Compile a TypeScript-syntax language directly to Solana SBF programs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -62,11 +62,12 @@
62
62
  #define TS_ERR_NO_RETURN 5114
63
63
  #define TS_ERR_ACCOUNT_MISSING 5115
64
64
 
65
- #define TS_FAIL(code, msg) \
66
- do { \
67
- sol_log(msg); \
68
- return (code); \
69
- } while (0)
65
+ /* Every failure logs through this one sol_log_ call: each syscall call site costs a 16-byte relocation in the binary. */
66
+ static __attribute__((noinline)) uint64_t ts_fail(uint64_t code, const char *msg, uint64_t len) {
67
+ sol_log_(msg, len);
68
+ return code;
69
+ }
70
+ #define TS_FAIL(code, msg) return ts_fail((code), (msg), sol_strlen(msg))
70
71
 
71
72
  #define TS_TRY(expr) \
72
73
  do { \
@@ -503,6 +504,49 @@ static inline void ts_write_le(uint8_t *p, uint64_t v, int n) { ts_memcpy(p, &v,
503
504
  uint64_t sol_get_rent_sysvar(void *ret);
504
505
  uint64_t sol_get_clock_sysvar(void *ret);
505
506
 
507
+ /* The general case, in integers: exactly (uint64_t)((double)per_year * threshold) as soft-float computes it (the
508
+ conversion and the product rounded to nearest, ties to even; NaN, negative and < 1 give 0), or UINT64_MAX when that
509
+ product is >= 2^64 (no double below 2^64 converts to UINT64_MAX: the largest is 2^64 - 2048). No soft-float routine
510
+ is linked, which keeps about 1.7 KB out of every program that creates accounts. */
511
+ static __attribute__((noinline, minsize)) uint64_t ts_rent_slow(uint64_t per_year, uint64_t tbits) {
512
+ uint64_t e = (tbits >> 52) & 0x7ff, f = tbits & 0xfffffffffffffULL;
513
+ if (e == 0x7ff) return !f && per_year && !(tbits >> 63) ? UINT64_MAX : 0; /* +inf overflows; NaN, inf * 0, -inf: 0 */
514
+ if ((tbits >> 63) || !per_year || (!e && !f)) return 0; /* a negative or zero product */
515
+ /* Round v * 2^ex (with a sticky bit for bits already dropped) to 53 bits, nearest-even, twice: per_year to a double,
516
+ then the exact product of the two significands. */
517
+ uint64_t v = per_year, sticky = 0, mt = e ? f | (1ULL << 52) : f;
518
+ int64_t ex = (e ? (int64_t)e : 1) - 1075;
519
+ for (int pass = 0; pass < 2; pass++) {
520
+ int top = 63;
521
+ while (!(v >> top)) top--;
522
+ if (top > 52) {
523
+ int sh = top - 52;
524
+ uint64_t rem = v & ((1ULL << sh) - 1), half = 1ULL << (sh - 1);
525
+ v >>= sh;
526
+ ex += sh;
527
+ if (rem > half || (rem == half && (sticky || (v & 1)))) v++;
528
+ if (v >> 53) v >>= 1, ex++;
529
+ }
530
+ if (pass) break;
531
+ /* v (< 2^53) * mt (< 2^53) as hi:lo, folded to 64 bits with the dropped low bits as the sticky bit. */
532
+ uint64_t a0 = v & 0xffffffff, a1 = v >> 32, b0 = mt & 0xffffffff, b1 = mt >> 32;
533
+ uint64_t lo = a0 * b0, mid = a0 * b1 + a1 * b0, hi = a1 * b1;
534
+ uint64_t lo2 = lo + (mid << 32);
535
+ hi += (mid >> 32) + (lo2 < lo);
536
+ lo = lo2;
537
+ int s = 0;
538
+ while (hi >> s) s++;
539
+ v = s ? (hi << (64 - s)) | (lo >> s) : lo;
540
+ sticky = s ? (lo & ((1ULL << s) - 1)) != 0 : 0;
541
+ ex += s;
542
+ }
543
+ int top = 63;
544
+ while (!(v >> top)) top--;
545
+ if (top + ex >= 64) return UINT64_MAX;
546
+ if (ex >= 0) return v << ex;
547
+ return ex > -64 ? v >> -ex : 0;
548
+ }
549
+
506
550
  /* The minimum balance of `data_len` bytes under `rent`, the Rent sysvar
507
551
  { lamports_per_byte_year: u64, exemption_threshold: f64, burn_percent: u8 }. */
508
552
  static inline uint64_t ts_minimum_balance_of(const uint64_t rent[3], uint64_t data_len, uint64_t *out) {
@@ -520,11 +564,9 @@ static inline uint64_t ts_minimum_balance_of(const uint64_t rent[3], uint64_t da
520
564
  return SUCCESS;
521
565
  }
522
566
  }
523
- double threshold;
524
- ts_memcpy(&threshold, &rent[1], 8);
525
- double lamports = (double)per_year * threshold;
526
- if (lamports >= 18446744073709551615.0) TS_FAIL(TS_ERR_OVERFLOW, "TypeSOL: arithmetic overflow");
527
- *out = (uint64_t)lamports;
567
+ uint64_t lamports = ts_rent_slow(per_year, rent[1]);
568
+ if (lamports == UINT64_MAX) TS_FAIL(TS_ERR_OVERFLOW, "TypeSOL: arithmetic overflow");
569
+ *out = lamports;
528
570
  return SUCCESS;
529
571
  }
530
572