@typesol/cli 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,81 @@ until 1.0, a minor version may change the language, the IDL layout or error code
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.18.0 — 2026-10-28
9
+
10
+ The remaining Anchor differences, closed: Anchor's argument structs (Strings, Vecs, Options and enums in Borsh),
11
+ keypair `init` with `space`, `init` of mints with Token-2022 extensions and of token accounts, `Vec<String>`
12
+ arguments, `SystemAccount`, string return values, and the Token-2022 and Token Metadata instructions programs call.
13
+ Closures, list callbacks and more everyday TypeScript; the rest of Bubblegum and Core. 26 of the 46 official
14
+ examples are now full ports. Existing programs keep their layouts and error codes (2018 and 3011 are new, raised only
15
+ by the new `Init<Mint>` and `SystemAccount` checks). Not externally audited (see SECURITY.md).
16
+
17
+ ### Added
18
+ Anchor accounts and arguments:
19
+ - `@layout borsh` structs as whole instruction arguments, return values and events, in Anchor's Borsh bytes (a
20
+ FixedString is a String, a FixedVec a Vec, an Option one byte when empty). Arguments are checked as Borsh
21
+ deserialization does, UTF-8 included (102 otherwise). `return vault` returns a whole struct account.
22
+ - `@layout borsh` structs used as arguments, return values or events may hold `string` fields and data enums (with or
23
+ without strings), as Anchor argument structs do; Borsh accounts may hold data enums without strings.
24
+ - `acct.create(payer, null)` on `Init<T>`: Anchor's `init` of a keypair account (it must sign; 3010 otherwise), and an
25
+ optional third argument for Anchor's `space = …` on `create` / `createIfNeeded`.
26
+ - Mint extensions on `Init<Mint>`: `mint.create(payer, seeds | null, decimals, authority, freeze, { … })` with
27
+ transferFee, closeAuthority, defaultAccountState, nonTransferable, interestBearing, permanentDelegate, transferHook,
28
+ metadataPointer, groupPointer and groupMemberPointer (Anchor's `extensions::…`).
29
+ - `Init<Mint>` / `InitIfNeeded<Mint>` with `mint.create(payer, seeds | null, decimals, mintAuthority, freezeAuthority)`
30
+ and `Init<TokenAccount>` / `InitIfNeeded<TokenAccount>` (without `Ata<…>`) with `vault.create(payer, seeds | null,
31
+ mint, authority)`: Anchor's `init` with `mint::` / `token::` constraints, at a PDA of this program or (with `null`) a
32
+ new keypair account that signs, under SPL Token or Token-2022 (token accounts sized with GetAccountDataSize). The
33
+ `createIfNeeded` forms check an existing account like Anchor's `init_if_needed` (2014 / 2015 / 2016 / 2017 and the
34
+ new 2018 ConstraintMintDecimals). The IDL marks keypair-created accounts as signers.
35
+ - `SystemAccount` / `Mut<SystemAccount>`: Anchor's `SystemAccount`, refused with 3011 (AccountNotSystemOwned) when
36
+ another program owns it.
37
+ - `Vec<string>` instruction arguments (Anchor's `Vec<String>`): `.get(i)`, `.length`, `for … of`, list callbacks,
38
+ helper parameters, storing into `FixedVec<FixedString<N>, M>`, and re-encoding through `invoke`; each string is
39
+ checked as UTF-8 on entry (102).
40
+ - Helpers and instructions can return `string` (an instruction's is Borsh String return data).
41
+ - `acct.key` of an `Init<…>` account before it is created (the address is known; Anchor seeds use it that way).
42
+ - `mint.create(…, tokenProgram)` / `vault.create(…, tokenProgram)`: an explicit token program, for instructions that
43
+ take more than one.
44
+
45
+ Language:
46
+ - Closures: a local function that reads or writes the enclosing function's variables is expanded where it is called,
47
+ with the names it was declared with (a block at the call that declares the same name does not change it), struct
48
+ parameters by reference and `return` leaving the closure. Recursion and storing / returning closures are refused.
49
+ - List callbacks on Vec, FixedArray, FixedVec and Vec arguments: `forEach`, `some`, `every`, `find`, `findIndex`,
50
+ `reduce`, `includes`, `indexOf`, expanded into loops with the arrow written at the call.
51
+ - Helpers can take function parameters (`f: (x: u64) => u64`): compiled once per named function passed.
52
+ - `do … while` loops (`continue` goes to the test).
53
+ - `x **= n` (the target must not call a function: it is read and written).
54
+ - `switch` on 128-bit integers (the case is picked with comparisons, then switched on as usual, fallthrough included).
55
+ - `?.` on optional accounts (`acct?.field`) and on `Option<struct>` fields (`x.home?.field`): an Option, empty when the
56
+ account was left out or the option is empty; use it with `??`.
57
+ - `acct!` on optional accounts (fails with 5115 when it was left out), Pubkeys / Hashes in template strings (base58),
58
+ and data-enum arguments whose variants carry fixed-size structs or arrays.
59
+ - Type parameters standing for fixed-size structs (`pick<Point>(…)`, or taken from struct arguments), and `cond ? a : b`
60
+ for struct values (places, literals or calls).
61
+ - Growable `Vec<T>` `push` / `pop` / `clear` inside helpers that take the account as `Mut<T>` (with its payer and
62
+ SystemProgram, as in instructions).
63
+ - `mulDiv` / `mulDivCeil` on u128 through a 256-bit product (all three u128; literals take that type).
64
+
65
+ CPI:
66
+ - Token-2022 extension instructions: `updateDefaultAccountState`, `updateTransferHook`, `enableRequiredMemoTransfers` /
67
+ `disableRequiredMemoTransfers`, `updateInterestRate`, `transferCheckedWithFee`, `harvestWithheldTokensToMint`,
68
+ `withdrawWithheldTokensFromMint`, `setTransferFee`; the Token Metadata interface: `updateMetadataField`,
69
+ `removeMetadataKey`, `updateMetadataAuthority`, `emitMetadata`; `closeAccount` of a mint; `mintTo` /
70
+ `initializeMetadata` on a mint created earlier in the instruction; `isTransferring(tokenAccount)` and
71
+ `transferHookAuthority(mint)`.
72
+ - Metaplex Core plugin values: MasterEdition, VerifiedCreators, Autograph, FreezeExecute, PermanentFreezeExecute, and
73
+ royalty rule sets (`ruleSet: { programAllowList | programDenyList }`).
74
+ - Bubblegum: `delegateV2`, `freezeV2`, `thawV2`, `setNonTransferableV2`, `updateMetadataV2`, `verifyCreatorV2`,
75
+ `unverifyCreatorV2`, `setCollectionV2`, and the V1 `transfer`, `burn`, `delegate`. Checked on the real programs with
76
+ every leaf recomputed: verify → delegate → freeze (a frozen leaf is refused) → thaw → rename, unverify, moving a leaf
77
+ into a collection, soulbinding a collection's leaf, and a V1 transfer signed by a PDA owner, delegate and burn.
78
+
79
+ ### Fixed
80
+ - Bubblegum leaf fields that may be null at run time (`assetDataHash: Hash | null`, `flags`) are sent as Borsh Options;
81
+ a null aborted the instruction before.
82
+
8
83
  ## 0.17.0 — 2026-10-28
9
84
 
10
85
  Anchor's account bytes on request (`@layout borsh`, so a TypeSOL program can take over an Anchor program's accounts
package/README.md CHANGED
@@ -189,10 +189,10 @@ Runtime failures point back to your source: `Error 6000 Unauthorized (counter.ts
189
189
 
190
190
  | | |
191
191
  | --- | --- |
192
- | **Accounts** | `Signer`, `Account<T>`, `Mut<T>`, `Init<T>`, `InitIfNeeded<T>` (PDAs and associated token accounts), `Zeroed<T>` (up to 10 MiB), optional accounts (`A \| null`), token accounts and mints, programs, sysvars |
192
+ | **Accounts** | `Signer`, `Account<T>`, `Mut<T>`, `Init<T>`, `InitIfNeeded<T>` (PDAs or keypair accounts, with Anchor's `space`; mints, token accounts and associated token accounts), `Zeroed<T>` (up to 10 MiB), optional accounts (`A \| null`), token accounts and mints, `SystemAccount`, programs, sysvars |
193
193
  | **Constraints** | `HasOne`, `Seeds` (also from account fields and under another program; `pdaBump` like `ctx.bumps`), `CloseTo`, `Ata`, `Address`, `Owner`, `Executable`. They are checked before the body runs and recorded in the IDL |
194
- | **Data** | `u8`…`u128`, `i8`…`i128`, `boolean`, `Pubkey`, `Hash`, nested structs, `FixedArray`, `FixedString`, `Option`, unit enums, data enums (discriminated unions), `Vec` that resizes its account, `FixedVec` lists anywhere, and Anchor's own byte layout (`@layout borsh`) to take over existing Anchor accounts |
195
- | **Control flow** | `if`, `for`, `for … of`, `while`, `switch` (with narrowing on data enums), ternaries, `??`, destructuring, helper functions (returning structs too, arrow functions, generics) across multiple files |
194
+ | **Data** | `u8`…`u128`, `i8`…`i128`, `boolean`, `Pubkey`, `Hash`, nested structs, `FixedArray`, `FixedString`, `Option`, unit enums, data enums (discriminated unions), `Vec` that resizes its account, `FixedVec` lists anywhere, and Anchor's own byte layout (`@layout borsh`) to take over existing Anchor accounts and to pass, return and emit structs with String / Vec / Option fields as Anchor does |
195
+ | **Control flow** | `if`, `for`, `for … of`, `while`, `switch` (with narrowing on data enums), ternaries, `??`, destructuring, helper functions (returning structs too, arrow functions, closures, function arguments, generics), list callbacks (`some`, `find`, `reduce`, …) across multiple files |
196
196
  | **CPIs** | System, SPL Token, Token-2022 (including extension reads and metadata), ATA, Metaplex Token Metadata, Core, Bubblegum. Generic `invoke` with PDA signing, return data, and typed bindings generated from any Anchor IDL (`typesol cpi`) |
197
197
  | **Events** | `emit` (log-based) and `emitCpi` (Anchor `emit_cpi!`, through a self-CPI) |
198
198
  | **Crypto & oracles** | `sha256`, `keccak256`, Merkle proofs, Ed25519 and secp256k1 signature checks, Pyth pull oracle, Switchboard |
@@ -306,7 +306,7 @@ Details are in [docs/testing.md](docs/testing.md).
306
306
 
307
307
  ## Status
308
308
 
309
- **TypeSOL is pre-1.0 (0.17).** Releases follow the organization's
309
+ **TypeSOL is pre-1.0 (0.18).** Releases follow the organization's
310
310
  [versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
311
311
  change.
312
312
  - The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
package/dist/clientgen.js CHANGED
@@ -39,6 +39,7 @@ export const TYPESOL_ERRORS = {
39
39
  2015: "ConstraintTokenOwner",
40
40
  2016: "ConstraintMintMintAuthority",
41
41
  2017: "ConstraintMintFreezeAuthority",
42
+ 2018: "ConstraintMintDecimals",
42
43
  2040: "ConstraintDuplicateMutableAccount",
43
44
  3000: "AccountDiscriminatorAlreadySet",
44
45
  3002: "AccountDiscriminatorMismatch",
@@ -50,6 +51,7 @@ export const TYPESOL_ERRORS = {
50
51
  3008: "InvalidProgramId",
51
52
  3009: "InvalidProgramExecutable",
52
53
  3010: "AccountNotSigner",
54
+ 3011: "AccountNotSystemOwned",
53
55
  3012: "AccountNotInitialized",
54
56
  3016: "AccountReallocExceedsLimit",
55
57
  4102: "InvalidNumericConversion",
@@ -12,7 +12,7 @@ export declare const MAX_CPI_ALLOCATION: number;
12
12
  export interface Analysis {
13
13
  model: ProgramModel;
14
14
  /** A generic helper's instance for these type arguments (analyzed once, then kept in `model.helpers`). */
15
- instantiate(g: GenericHelper, typeArgs: ScalarType[]): Helper;
15
+ instantiate(g: GenericHelper, typeArgs: TypeArg[], fnArgs?: string[]): Helper;
16
16
  /** TypeScript's checker: used for narrowing (which variant a data-enum value is in a branch). */
17
17
  checker: ts.TypeChecker;
18
18
  sourceFile: ts.SourceFile;
@@ -22,11 +22,20 @@ export interface Analysis {
22
22
  imports: Map<string, string>;
23
23
  reporter: Reporter;
24
24
  }
25
+ /** What a generic helper's type parameter stands for: a scalar, or a fixed-size struct. */
26
+ export type TypeArg = ScalarType | {
27
+ kind: "struct";
28
+ name: string;
29
+ };
30
+ /** A struct named by a type reference, through the type parameters of a generic helper's instance. */
31
+ export declare function structNameOf(node: ts.TypeNode, scope: Pick<TypeScope, "structArgs">): string | undefined;
25
32
  /** What type resolution needs: imports, unit enums, and where to record Option<T> uses. */
26
33
  export interface TypeScope {
27
34
  imports: Map<string, string>;
28
35
  /** Inside a generic helper's instance: type parameter -> the type it stands for. */
29
36
  typeArgs?: Map<string, ScalarType>;
37
+ /** Inside a generic helper's instance: type parameter -> the struct it stands for. */
38
+ structArgs?: Map<string, string>;
30
39
  enums: Map<string, EnumDef>;
31
40
  dataEnums?: Map<string, DataEnumDef>;
32
41
  options: Map<string, ScalarType | {
@@ -90,6 +90,7 @@ const TYPE_EXPORTS = new Set([
90
90
  "SelfProgram",
91
91
  "ProgramData",
92
92
  "SignerSeeds",
93
+ "SystemAccount",
93
94
  ]);
94
95
  /** Largest fixed array a struct may declare, and largest FixedString. */
95
96
  const MAX_ARRAY_LEN = 1_000_000;
@@ -99,6 +100,12 @@ export const MAX_ACCOUNT_SIZE = 10 * 1024 * 1024;
99
100
  /** sol_set_return_data's limit. */
100
101
  export const MAX_RETURN_DATA = 1024;
101
102
  export const MAX_CPI_ALLOCATION = 10 * 1024;
103
+ /** A struct named by a type reference, through the type parameters of a generic helper's instance. */
104
+ export function structNameOf(node, scope) {
105
+ if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName) || node.typeArguments?.length)
106
+ return undefined;
107
+ return scope.structArgs?.get(node.typeName.text) ?? node.typeName.text;
108
+ }
102
109
  export function snakeCase(name) {
103
110
  return name
104
111
  .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
@@ -424,23 +431,35 @@ export function analyze(entry) {
424
431
  if (h.node.body)
425
432
  collectLocals(h.node.body, h.name, h.imports, h.node);
426
433
  const localDecls = new Set(locals.map((l) => l.decl));
427
- for (const l of locals)
428
- reporter.guard(() => checkCaptures(l, localDecls, checker, reporter));
434
+ // A local function that reads the enclosing function's variables (or calls one that does) is a closure: it is
435
+ // expanded where it is called, in the enclosing function. The others become helpers of their own.
436
+ const inline = new Set(locals.filter((l) => captures(l, localDecls, checker)).map((l) => l.decl));
437
+ for (let changed = true; changed;) {
438
+ changed = false;
439
+ for (const l of locals)
440
+ if (!inline.has(l.decl) && references(l, inline, checker)) {
441
+ inline.add(l.decl);
442
+ changed = true;
443
+ }
444
+ }
445
+ const closureDecls = new Map(locals.filter((l) => inline.has(l.decl)).map((l) => [l.decl, l.display]));
429
446
  const helpers = new Map();
430
447
  const generics = new Map();
431
448
  const helperDecls = new Map();
432
- for (const fn of [...helperNodes, ...locals]) {
449
+ for (const fn of [...helperNodes, ...locals.filter((l) => !inline.has(l.decl))]) {
433
450
  helperDecls.set(fn.decl, fn.name);
434
- if (fn.node.typeParameters?.length) {
451
+ const fnParams = fn.node.parameters.map((p, i) => (p.type && ts.isFunctionTypeNode(p.type) ? i : -1)).filter((i) => i >= 0);
452
+ if (fn.node.typeParameters?.length || fnParams.length) {
435
453
  reporter.guard(() => {
436
- for (const tp of fn.node.typeParameters)
454
+ for (const tp of fn.node.typeParameters ?? [])
437
455
  if (tp.default)
438
456
  reporter.fail(tp, "type parameters of helpers take no default");
439
457
  generics.set(fn.name, {
440
458
  name: fn.name,
441
459
  display: fn.display,
442
460
  node: fn.node,
443
- typeParams: fn.node.typeParameters.map((t) => t.name.text),
461
+ typeParams: (fn.node.typeParameters ?? []).map((t) => t.name.text),
462
+ fnParams,
444
463
  imports: fn.imports,
445
464
  });
446
465
  });
@@ -451,20 +470,34 @@ export function analyze(entry) {
451
470
  helpers.set(h.name, h);
452
471
  }
453
472
  const instances = new Map();
454
- const instantiate = (g, typeArgs) => {
455
- const key = `${g.name}__${typeArgs.map(scalarKey).join("_")}`;
473
+ const instantiate = (g, typeArgs, fnArgs = []) => {
474
+ const argKey = (t) => (t.kind === "struct" ? `S${t.name}` : scalarKey(t));
475
+ const key = [g.name, ...typeArgs.map(argKey), ...fnArgs.map((f) => `fn_${f}`)].join("__");
456
476
  const done = instances.get(key);
457
477
  if (done)
458
478
  return done;
459
- const bound = new Map(g.typeParams.map((n, i) => [n, typeArgs[i]]));
460
- const display = `${g.display}<${typeArgs.map(typeName).join(", ")}>`;
461
- const h = analyzeHelper(g.node, key, display, reporter, { ...scopeFor(g.imports), typeArgs: bound }, structs);
479
+ const bound = new Map();
480
+ const boundStructs = new Map();
481
+ g.typeParams.forEach((n, i) => {
482
+ const t = typeArgs[i];
483
+ if (t.kind === "struct")
484
+ boundStructs.set(n, t.name);
485
+ else
486
+ bound.set(n, t);
487
+ });
488
+ const display = `${g.display}<${[...typeArgs.map((t) => (t.kind === "struct" ? t.name : typeName(t))), ...fnArgs].join(", ")}>`;
489
+ const h = analyzeHelper(g.node, key, display, reporter, { ...scopeFor(g.imports), typeArgs: bound, structArgs: boundStructs }, structs, new Set(g.fnParams));
462
490
  h.typeArgs = bound;
491
+ h.structArgs = boundStructs;
492
+ if (g.fnParams.length) {
493
+ h.fnArgs = new Map(g.fnParams.map((i, k) => [g.node.parameters[i].name.text, fnArgs[k]]));
494
+ h.fnArgIndexes = g.fnParams;
495
+ }
463
496
  instances.set(key, h);
464
497
  helpers.set(key, h);
465
498
  return h;
466
499
  };
467
- reporter.guard(() => checkRecursion([...helperNodes, ...locals], helperDecls, checker, reporter));
500
+ reporter.guard(() => checkRecursion([...helperNodes, ...locals.filter((l) => !inline.has(l.decl))], helperDecls, checker, reporter));
468
501
  const accountStructs = new Set();
469
502
  const argStructs = new Set();
470
503
  const instructions = [];
@@ -478,26 +511,29 @@ export function analyze(entry) {
478
511
  for (const a of ix.args) {
479
512
  const seen = new Set();
480
513
  collectStructs(a.type, structs, dataEnums, seen);
514
+ // A @layout borsh struct is decoded as a whole argument; inside lists or enums it would need a fixed size.
515
+ const whole = a.type.kind === "structRef" ? a.type.struct : undefined;
481
516
  for (const s of seen)
482
- if (structs.get(s).layout === "borsh")
483
- reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} is @layout borsh, an account layout only`);
517
+ if (structs.get(s).layout === "borsh" && s !== whole)
518
+ reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} is @layout borsh; pass it as a whole argument, not inside a list or enum`);
484
519
  }
485
- if (ix.returns?.kind === "struct" && structs.get(ix.returns.name).layout === "borsh")
486
- reporter.error(fn.node.type ?? fn.node, `${ix.returns.name} is @layout borsh, an account layout only; return another struct`);
487
- // Arguments are Borsh: a FixedVec's padded layout is account data only.
520
+ // Arguments are Borsh: a FixedVec's padded layout is account data only (a @layout borsh struct's FixedVec is a Borsh Vec).
488
521
  for (const a of ix.args) {
489
522
  const seen = new Set();
490
523
  collectStructs(a.type, structs, dataEnums, seen);
491
524
  for (const s of seen)
492
- if (structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
525
+ if (structs.get(s).layout !== "borsh" && structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
493
526
  reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} holds a FixedVec, which only account data can; pass a Vec<T> argument instead`);
494
527
  }
495
528
  // A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
496
529
  if (ix.returns?.kind === "struct")
497
530
  collectStructs(ix.returns, structs, dataEnums, argStructs);
498
531
  for (const a of ix.accounts)
499
- if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded")
532
+ if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded") {
500
533
  accountStructs.add(a.kind.struct.name);
534
+ if (a.kind.struct.viewsOnly)
535
+ reporter.error(a.node, `${a.kind.struct.name} holds a string (or an enum carrying one), so it is an argument, return value or event; account data uses FixedString<N>`);
536
+ }
501
537
  }
502
538
  checkSnakeDuplicates(instructions.map((ix) => ({ name: ix.node.name.text, node: ix.node.name })), "instructions", reporter);
503
539
  // emitCpi's event instruction (Anchor's EVENT_IX_TAG) is dispatched before every other instruction.
@@ -530,6 +566,7 @@ export function analyze(entry) {
530
566
  helpers,
531
567
  generics,
532
568
  helperDecls,
569
+ closureDecls,
533
570
  errors,
534
571
  errorEnums,
535
572
  enums,
@@ -585,7 +622,7 @@ export function resolveArgAggregate(node, scope, structs) {
585
622
  if (scalar)
586
623
  return scalar;
587
624
  if (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && !t.typeArguments?.length) {
588
- const s = structs.get(t.typeName.text);
625
+ const s = structs.get(structNameOf(t, scope));
589
626
  if (s && s.size !== undefined && !s.vec)
590
627
  return { kind: "struct", name: s.name };
591
628
  }
@@ -668,6 +705,9 @@ export function resolveValueType(node, scope, allowVec = false) {
668
705
  return { kind: "dataEnumArg", name: node.typeName.text };
669
706
  if (allowVec && typesolName(node, scope.imports) === "Vec") {
670
707
  const elemNode = node.typeArguments?.[0];
708
+ // Vec<string>: Borsh Vec<String>, read as views of the instruction data.
709
+ if (elemNode?.kind === ts.SyntaxKind.StringKeyword)
710
+ return { kind: "vecArg", elem: { kind: "string" } };
671
711
  const elem = elemNode && resolveScalarType(elemNode, scope);
672
712
  if (elem)
673
713
  return { kind: "vecArg", elem };
@@ -935,7 +975,11 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
935
975
  state.set(s.name, "visiting");
936
976
  let total = 0;
937
977
  for (const f of s.fields) {
938
- const fsz = sizeOf(f.type, s.node);
978
+ // In a @layout borsh struct a string (or a data enum carrying one) is held as a TS_Str view: a C size, no layout.
979
+ const view = s.layout === "borsh" && (f.type.kind === "string" || (f.type.kind === "dataEnum" && sizeOf(f.type, s.node) === undefined));
980
+ if (view)
981
+ s.viewsOnly = true;
982
+ const fsz = view ? (f.type.kind === "string" ? 16 : 1 + dataEnums.get(f.type.name).payload) : sizeOf(f.type, s.node);
939
983
  total = total === undefined || fsz === undefined ? undefined : total + fsz;
940
984
  }
941
985
  s.size = total;
@@ -954,19 +998,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
954
998
  reporter.guard(() => visit(s));
955
999
  for (const s of structs.values())
956
1000
  if (s.layout === "borsh")
957
- reporter.guard(() => checkBorshLayout(s, structs, reporter));
1001
+ reporter.guard(() => checkBorshLayout(s, structs, dataEnums, reporter));
958
1002
  for (const e of dataEnums.values())
959
1003
  reporter.guard(() => sizeOf({ kind: "dataEnum", name: e.name }, e.node));
960
1004
  return structs;
961
1005
  }
962
1006
  const isFunctionNode = (n) => ts.isFunctionDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isMethodDeclaration(n);
963
- /**
964
- * A local function becomes a helper of its own, so it can only use its parameters, its own locals, top-level names and
965
- * other local functions: reading a variable of the enclosing function (a closure) is refused.
966
- */
967
- function checkCaptures(l, localDecls, checker, reporter) {
1007
+ /** Whether a local function reads a variable or parameter of a function around it (a closure). */
1008
+ function captures(l, localDecls, checker) {
968
1009
  const fn = l.node;
1010
+ let found = false;
969
1011
  const visit = (n) => {
1012
+ if (found)
1013
+ return;
970
1014
  if (ts.isIdentifier(n)) {
971
1015
  const sym = ts.isShorthandPropertyAssignment(n.parent) ? checker.getShorthandAssignmentValueSymbol(n.parent) : checker.getSymbolAtLocation(n);
972
1016
  for (const d of sym?.declarations ?? []) {
@@ -974,30 +1018,53 @@ function checkCaptures(l, localDecls, checker, reporter) {
974
1018
  continue;
975
1019
  if (d.getSourceFile() !== fn.getSourceFile() || (d.pos >= fn.pos && d.end <= fn.end))
976
1020
  continue;
977
- // Declared in some function around this one: a captured variable.
978
1021
  for (let p = d.parent; p; p = p.parent)
979
- if (isFunctionNode(p)) {
980
- reporter.fail(n, `${l.display} reads "${n.text}" of the function around it; pass it as a parameter (local functions capture nothing)`);
981
- }
1022
+ if (isFunctionNode(p))
1023
+ found = true;
982
1024
  }
983
1025
  }
984
1026
  ts.forEachChild(n, visit);
985
1027
  };
986
1028
  if (fn.body)
987
1029
  visit(fn.body);
1030
+ return found;
1031
+ }
1032
+ /** Whether a function's body names one of `decls` (a closure it calls). */
1033
+ function references(l, decls, checker) {
1034
+ let found = false;
1035
+ const visit = (n) => {
1036
+ if (found)
1037
+ return;
1038
+ if (ts.isIdentifier(n))
1039
+ for (const d of checker.getSymbolAtLocation(n)?.declarations ?? [])
1040
+ if (decls.has(d))
1041
+ found = true;
1042
+ ts.forEachChild(n, visit);
1043
+ };
1044
+ if (l.node.body)
1045
+ visit(l.node.body);
1046
+ return found;
988
1047
  }
989
1048
  /**
990
1049
  * A `@layout borsh` struct: its own fields may be FixedString (Borsh String), FixedVec (Borsh Vec) and Option; anything
991
1050
  * nested must already be Borsh-identical (fixed-size scalars, Pubkeys, enums, arrays and structs of them).
992
1051
  */
993
- function checkBorshLayout(s, structs, reporter) {
1052
+ function checkBorshLayout(s, structs, dataEnums, reporter) {
994
1053
  const model = { structs };
995
1054
  const fixed = (t) => borshIdentical(t, model);
996
1055
  for (const f of s.fields) {
997
1056
  const t = f.type;
998
1057
  const at = s.node.members.find((m) => m.name && ts.isIdentifier(m.name) && m.name.text === f.name) ?? s.node;
999
- if (fixed(t) || t.kind === "fstring")
1058
+ if (fixed(t) || t.kind === "fstring" || t.kind === "string")
1000
1059
  continue;
1060
+ if (t.kind === "dataEnum") {
1061
+ // Borsh enum: tag, then the variant's fields; each a fixed-size value or a string.
1062
+ for (const v of dataEnums.get(t.name)?.variants ?? [])
1063
+ for (const vf of v.fields)
1064
+ if (!fixed(vf.type) && vf.type.kind !== "string")
1065
+ reporter.fail(at, `${t.name}.${v.name}.${vf.name} in a @layout borsh struct: enum variants hold fixed-size values and strings`);
1066
+ continue;
1067
+ }
1001
1068
  if (t.kind === "vec" && t.cap === undefined)
1002
1069
  reporter.fail(at, "a @layout borsh struct cannot end in a growable Vec<T>; use FixedVec<T, N> (Anchor's #[max_len(N)] Vec)");
1003
1070
  if (t.kind === "vec" && (fixed(t.elem) || t.elem.kind === "fstring"))
@@ -1015,7 +1082,7 @@ function checkBorshLayout(s, structs, reporter) {
1015
1082
  reporter.error(s.node.name, `${s.name} is @layout borsh, so it is account data only; ${other.name}.${f.name} cannot hold one`);
1016
1083
  }
1017
1084
  }
1018
- function analyzeHelper(fn, name, display, reporter, scope, structs) {
1085
+ function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new Set()) {
1019
1086
  if (!fn.body)
1020
1087
  reporter.fail(fn, "helper functions need a body");
1021
1088
  if (fn.asteriskToken)
@@ -1024,16 +1091,23 @@ function analyzeHelper(fn, name, display, reporter, scope, structs) {
1024
1091
  reporter.fail(fn, "helper functions cannot be async");
1025
1092
  if (!fn.type)
1026
1093
  reporter.fail(fn.name ?? fn, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
1027
- let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword ? undefined : resolveScalarType(fn.type, scope);
1094
+ let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword
1095
+ ? undefined
1096
+ : fn.type.kind === ts.SyntaxKind.StringKeyword
1097
+ ? { kind: "string" }
1098
+ : resolveScalarType(fn.type, scope);
1028
1099
  if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns) {
1029
- const s = ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments ? structs.get(fn.type.typeName.text) : undefined;
1100
+ const sn = structNameOf(fn.type, scope);
1101
+ const s = sn ? structs.get(sn) : undefined;
1030
1102
  if (s && (s.size === undefined || s.vec))
1031
1103
  reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be returned; return a struct of fixed size`);
1032
1104
  if (!s)
1033
- reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, a struct or a type parameter");
1105
+ reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, string, a struct or a type parameter");
1034
1106
  returns = { kind: "struct", name: s.name };
1035
1107
  }
1036
- const params = fn.parameters.map((p) => {
1108
+ const params = fn.parameters
1109
+ .filter((_, i) => !skip.has(i))
1110
+ .map((p) => {
1037
1111
  if (!ts.isIdentifier(p.name) || !p.type || p.initializer || p.questionToken || p.dotDotDotToken)
1038
1112
  reporter.fail(p, "parameters need a plain name and an explicit type");
1039
1113
  // `p_<name>__n` carries a SignerSeeds count: "__" in a name could collide with it.
@@ -1120,7 +1194,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1120
1194
  reporter.fail(fn, "instructions cannot be async or generators");
1121
1195
  let returns;
1122
1196
  if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword) {
1123
- returns = resolveScalarType(fn.type, scope);
1197
+ returns = fn.type.kind === ts.SyntaxKind.StringKeyword ? { kind: "string" } : resolveScalarType(fn.type, scope);
1124
1198
  const s = !returns && ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments
1125
1199
  ? structs.get(fn.type.typeName.text)
1126
1200
  : undefined;
@@ -1133,7 +1207,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1133
1207
  returns = { kind: "struct", name: s.name };
1134
1208
  }
1135
1209
  if (!returns)
1136
- reporter.fail(fn.type, "instructions return void, one integer, boolean or Pubkey, or a fixed-size struct (sent as return data)");
1210
+ reporter.fail(fn.type, "instructions return void, one integer, boolean, Pubkey or string, or a fixed-size struct (sent as return data)");
1137
1211
  }
1138
1212
  const accounts = [];
1139
1213
  const args = [];
@@ -1173,7 +1247,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1173
1247
  const inner = unwrapConstraints(typeNode, reporter, imports, constraints);
1174
1248
  const kind = resolveAccountKind(inner, reporter, imports, structs);
1175
1249
  if (optional &&
1176
- (["init", "initIfNeeded", "zeroed", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) || (kind.kind === "token" && kind.init)))
1250
+ (["init", "initIfNeeded", "zeroed", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
1251
+ ((kind.kind === "token" || kind.kind === "mint") && kind.init)))
1177
1252
  reporter.fail(p, "Init, InitIfNeeded, Zeroed, PythPriceUpdate, EventAuthority, SelfProgram and ProgramData accounts cannot be optional");
1178
1253
  accounts.push({ name: p.name.text, index: accounts.length, kind, node: p, constraints, ...(optional ? { optional } : {}) });
1179
1254
  });
@@ -1186,10 +1261,11 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1186
1261
  // Constraints refer to other parameters by name: check them now that all are known.
1187
1262
  for (const a of accounts) {
1188
1263
  const c = a.constraints;
1189
- if (a.kind.kind === "token" && a.kind.init && !c.ata)
1190
- reporter.fail(a.node, 'Init<TokenAccount> / InitIfNeeded<TokenAccount> create associated token accounts: wrap them in Ata<…, "owner", "mint">');
1191
- if (a.kind.kind === "token" && a.kind.init && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
1264
+ if (a.kind.kind === "token" && a.kind.init && c.ata && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
1192
1265
  reporter.fail(a.node, "an associated token account created here is checked against Ata<…>'s owner and mint; drop HasOne/Seeds/Address/Owner/Executable");
1266
+ // Token accounts and mints created in the body are checked there (seeds, mint, authorities).
1267
+ if ((a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init && !c.ata && (c.hasOne.length || c.seeds || c.address || c.owner || c.executable))
1268
+ reporter.fail(a.node, `${a.name}.create(...) takes the seeds, mint and authorities; drop HasOne/Seeds/Address/Owner/Executable`);
1193
1269
  if (!c.hasOne.length && !c.seeds && !c.closeTo && !c.ata)
1194
1270
  continue;
1195
1271
  const struct = a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || a.kind.kind === "zeroed" ? a.kind.struct : undefined;
@@ -1381,6 +1457,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1381
1457
  // Any account (helpers that accept every kind, e.g. generated CPI helpers).
1382
1458
  case "AccountMeta":
1383
1459
  return { kind: "info", mut: false };
1460
+ case "SystemAccount":
1461
+ return { kind: "info", mut: false, systemOwned: true };
1384
1462
  case "TokenAccount":
1385
1463
  return { kind: "token", mut: false };
1386
1464
  case "Mint":
@@ -1438,9 +1516,11 @@ function resolveAccountKind(node, reporter, imports, structs) {
1438
1516
  case "Init": {
1439
1517
  if (typesolName(inner(), imports) === "TokenAccount")
1440
1518
  return { kind: "token", mut: true, init: "create" };
1519
+ if (typesolName(inner(), imports) === "Mint")
1520
+ return { kind: "mint", mut: true, init: "create" };
1441
1521
  const s = structOf(inner());
1442
1522
  if (!s)
1443
- reporter.fail(node, "Init<T> needs a struct declared in this program");
1523
+ reporter.fail(node, "Init<T> needs a struct declared in this program, TokenAccount or Mint");
1444
1524
  if (s.size + 8 > MAX_CPI_ALLOCATION)
1445
1525
  reporter.fail(node, `${s.name} needs ${s.size + 8} bytes but a program can create at most ${MAX_CPI_ALLOCATION} through Init<T>; have the client allocate it and take it as Zeroed<${s.name}>`);
1446
1526
  return { kind: "init", struct: s };
@@ -1448,6 +1528,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1448
1528
  case "InitIfNeeded": {
1449
1529
  if (typesolName(inner(), imports) === "TokenAccount")
1450
1530
  return { kind: "token", mut: true, init: "ifNeeded" };
1531
+ if (typesolName(inner(), imports) === "Mint")
1532
+ return { kind: "mint", mut: true, init: "ifNeeded" };
1451
1533
  const s = structOf(inner());
1452
1534
  if (!s)
1453
1535
  reporter.fail(node, "InitIfNeeded<T> needs a struct declared in this program");
@@ -1475,6 +1557,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1475
1557
  return { kind: "signer", mut: true };
1476
1558
  if (innerName === "AccountInfo" || innerName === "AccountMeta")
1477
1559
  return { kind: "info", mut: true };
1560
+ if (innerName === "SystemAccount")
1561
+ return { kind: "info", mut: true, systemOwned: true };
1478
1562
  if (innerName === "TokenAccount")
1479
1563
  return { kind: "token", mut: true };
1480
1564
  if (innerName === "Mint")
@@ -2,7 +2,7 @@ import ts from "typescript";
2
2
  import { type Analysis, type TypeScope } from "../analyze.js";
3
3
  import type { Reporter } from "../diagnostics.js";
4
4
  import { type AccountParam, type Helper, type IdlSeed, type Instruction, type ProgramModel, type ValueType } from "../model.js";
5
- import { type Value, type Local, type EmitOptions } from "./shared.js";
5
+ import { type Value, type Ref, type Local, type Closure, type EmitOptions } from "./shared.js";
6
6
  /**
7
7
  * State of one function being lowered, and the primitives every layer uses: emitting lines, temporaries,
8
8
  * scopes, evaluation-order snapshots and account lookups.
@@ -24,6 +24,17 @@ 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
+ /** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
28
+ protected inlineFrames: {
29
+ label: string;
30
+ result: string;
31
+ type?: ValueType;
32
+ declAt: number;
33
+ pad: string;
34
+ node: ts.Node;
35
+ }[];
36
+ /** Closures being expanded (a closure calling itself cannot be expanded). */
37
+ protected readonly inlining: Set<ts.Node>;
27
38
  /** Enclosing loops, innermost last; each has a `continue` label. */
28
39
  protected readonly loops: string[];
29
40
  protected readonly ix: Instruction | undefined;
@@ -39,6 +50,14 @@ export declare abstract class EmitterBase {
39
50
  constructor(analysis: Analysis, target: Instruction | Helper, opts?: EmitOptions);
40
51
  /** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
41
52
  protected helperOf(node: ts.CallExpression): Helper | undefined;
53
+ /**
54
+ * The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
55
+ * captures nothing). A closure can only run where it is declared, so it cannot be passed.
56
+ */
57
+ private fnArgsOf;
58
+ /** A struct written as a type argument (`pick<Point>(…)`). */
59
+ private structArg;
60
+ private fixedStruct;
42
61
  /** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
43
62
  private typeArgsOf;
44
63
  protected line(s: string): void;
@@ -59,11 +78,17 @@ export declare abstract class EmitterBase {
59
78
  protected block(fn: () => void): void;
60
79
  protected fail(node: ts.Node, message: string): never;
61
80
  protected exportName(node: ts.Expression): string | undefined;
81
+ /**
82
+ * `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
83
+ * failing with 3020 otherwise). Returns undefined for anything else.
84
+ */
85
+ protected withoutBang(node: ts.Expression): ts.PropertyAccessExpression | undefined;
62
86
  protected accountParam(node: ts.Expression, what: string): AccountParam;
63
87
  protected assertUsable(a: AccountParam, node: ts.Node): void;
64
88
  protected abstract expect(v: Value, want: ValueType | undefined, node: ts.Node): Value;
65
89
  protected abstract literal(value: bigint, type: ValueType, node: ts.Node): Value;
66
90
  protected abstract expr(node: ts.Expression, hint?: ValueType): Value;
91
+ protected abstract ref(node: ts.Expression): Ref | undefined;
67
92
  protected abstract call(node: ts.CallExpression, topLevel: boolean): Value | undefined;
68
93
  protected abstract isRemaining(node: ts.Expression): boolean;
69
94
  protected abstract handle(node: ts.Expression): string | undefined;
@@ -75,6 +100,7 @@ export declare abstract class EmitterBase {
75
100
  protected abstract recordPda(account: AccountParam, idl: IdlSeed[] | undefined): void;
76
101
  protected abstract create(node: ts.CallExpression, target: AccountParam, topLevel: boolean, ifNeeded?: boolean): Value;
77
102
  protected abstract createAta(node: ts.CallExpression, target: AccountParam, topLevel: boolean): Value | undefined;
103
+ protected abstract createToken(node: ts.CallExpression, target: AccountParam, topLevel: boolean): Value;
78
104
  protected abstract systemTransfer(node: ts.CallExpression): undefined;
79
105
  protected abstract createAccount(node: ts.CallExpression, system: AccountParam): undefined;
80
106
  protected abstract tokenOp(node: ts.CallExpression, program: AccountParam, method: string): undefined;
@@ -88,4 +114,6 @@ export declare abstract class EmitterBase {
88
114
  program: string;
89
115
  }): undefined;
90
116
  protected abstract epilogue(): string[];
117
+ protected abstract expandClosure(c: Closure, args: readonly ts.Expression[], node: ts.Node): Value | undefined;
118
+ protected abstract listMethod(node: ts.CallExpression, method: string): Value | undefined;
91
119
  }