@typesol/cli 0.16.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.
@@ -4,7 +4,8 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { CompileError, Reporter } from "./diagnostics.js";
6
6
  import { decodeAddress } from "../keys.js";
7
- import { optionKey, INT_INFO, INT_NAMES, scalarKey, isScalar, scalarSize, } from "./model.js";
7
+ import { borshIdentical } from "./emit/borsh.js";
8
+ import { optionKey, INT_INFO, INT_NAMES, scalarKey, fixedVecName, isScalar, scalarSize, typeName, } from "./model.js";
8
9
  export const LIB_PATH = fileURLToPath(new URL("../../lib/typesol.d.ts", import.meta.url));
9
10
  /** Names a struct field may not use because account parameters already expose them. */
10
11
  const RESERVED_FIELDS = new Set(["key", "lamports", "dataLen", "create", "equals", "get", "set", "length", "isSigner", "isWritable", "pdaBump"]);
@@ -57,6 +58,7 @@ const TYPE_EXPORTS = new Set([
57
58
  "TokenInterface",
58
59
  "FixedArray",
59
60
  "FixedString",
61
+ "FixedVec",
60
62
  "Program",
61
63
  "UncheckedProgram",
62
64
  "CpiAccount",
@@ -88,6 +90,7 @@ const TYPE_EXPORTS = new Set([
88
90
  "SelfProgram",
89
91
  "ProgramData",
90
92
  "SignerSeeds",
93
+ "SystemAccount",
91
94
  ]);
92
95
  /** Largest fixed array a struct may declare, and largest FixedString. */
93
96
  const MAX_ARRAY_LEN = 1_000_000;
@@ -97,6 +100,12 @@ export const MAX_ACCOUNT_SIZE = 10 * 1024 * 1024;
97
100
  /** sol_set_return_data's limit. */
98
101
  export const MAX_RETURN_DATA = 1024;
99
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
+ }
100
109
  export function snakeCase(name) {
101
110
  return name
102
111
  .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
@@ -225,7 +234,7 @@ export function analyze(entry) {
225
234
  const isExported = (n) => ts.canHaveModifiers(n) && (ts.getModifiers(n) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
226
235
  const declare = (name, file) => {
227
236
  // These prefixes name the IDL types TypeSOL generates for FixedString<N>, Option and data enums in account data.
228
- if (/^(FixedString\d|FixedOption_|FixedEnum_)/.test(name.text))
237
+ if (/^(FixedString\d|FixedVec\d|FixedOption_|FixedEnum_)/.test(name.text))
229
238
  reporter.fail(name, `"${name.text}" is reserved for IDL types TypeSOL generates; pick another name`);
230
239
  const prev = declared.get(name.text);
231
240
  if (prev)
@@ -313,7 +322,8 @@ export function analyze(entry) {
313
322
  else {
314
323
  if (stmt.name)
315
324
  declare(stmt.name, file);
316
- helperNodes.push({ node: stmt, imports });
325
+ if (stmt.name)
326
+ helperNodes.push({ node: stmt, name: stmt.name.text, display: stmt.name.text, decl: stmt, imports });
317
327
  }
318
328
  }
319
329
  else if (ts.isVariableStatement(stmt)) {
@@ -324,6 +334,14 @@ export function analyze(entry) {
324
334
  if (!ts.isIdentifier(d.name) || !d.initializer)
325
335
  reporter.fail(d, "declare const NAME = literal");
326
336
  const init = d.initializer;
337
+ // `const f = (…): T => …` / `const f = function (…) {…}`: a helper.
338
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
339
+ if (isEntry && isExported(stmt))
340
+ reporter.fail(d.name, "instructions are function declarations: export function name(…)");
341
+ declare(d.name, file);
342
+ helperNodes.push({ node: init, name: d.name.text, display: d.name.text, decl: d, imports });
343
+ continue;
344
+ }
327
345
  let value;
328
346
  if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && imports.get(init.expression.text) === "pubkey") {
329
347
  const arg = init.arguments[0];
@@ -358,6 +376,7 @@ export function analyze(entry) {
358
376
  }
359
377
  }
360
378
  const fstrings = new Set();
379
+ const fixedVecs = new Map();
361
380
  const scopeFor = (imports) => ({ imports, enums, options, dataEnums });
362
381
  for (const c of pendingConstTypes) {
363
382
  reporter.guard(() => {
@@ -372,14 +391,113 @@ export function analyze(entry) {
372
391
  c.def.type = t;
373
392
  });
374
393
  }
375
- const structs = analyzeStructs(interfaces, aliases, dataEnums, reporter, scopeFor, fstrings);
394
+ const structs = analyzeStructs(interfaces, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs);
395
+ const checker = program.getTypeChecker();
396
+ // Local `const f = (…) => …` in any function body (and in those local functions): lifted to helpers.
397
+ const locals = [];
398
+ const taken = new Set();
399
+ const collectLocals = (body, outer, imports, fn) => {
400
+ const generic = !!fn.typeParameters?.length;
401
+ const visit = (n) => {
402
+ if (ts.isVariableDeclaration(n) && n.initializer && (ts.isArrowFunction(n.initializer) || ts.isFunctionExpression(n.initializer))) {
403
+ reporter.guard(() => {
404
+ if (!ts.isIdentifier(n.name))
405
+ reporter.fail(n.name, "name local functions: const f = (…) => …");
406
+ if (generic)
407
+ reporter.fail(n, "local functions inside a generic helper are not supported; declare this one at the top level");
408
+ if (!(n.parent.flags & ts.NodeFlags.Const) || !ts.isVariableStatement(n.parent.parent))
409
+ reporter.fail(n, "declare local functions with const: const f = (…) => …");
410
+ if (n.name.text.includes("__") || n.name.text.includes("$"))
411
+ reporter.fail(n.name, `"${n.name.text}": "__" and "$" are reserved in names`);
412
+ let name = `${outer}__${n.name.text}`;
413
+ for (let i = 2; taken.has(name); i++)
414
+ name = `${outer}__${n.name.text}__${i}`;
415
+ taken.add(name);
416
+ const src = { node: n.initializer, name, display: n.name.text, decl: n, imports, outer: fn };
417
+ locals.push(src);
418
+ if (src.node.body)
419
+ collectLocals(src.node.body, name, imports, src.node);
420
+ });
421
+ return;
422
+ }
423
+ ts.forEachChild(n, visit);
424
+ };
425
+ visit(body);
426
+ };
427
+ for (const f of functions)
428
+ if (f.node.body && f.node.name)
429
+ collectLocals(f.node.body, f.node.name.text, f.imports, f.node);
430
+ for (const h of [...helperNodes])
431
+ if (h.node.body)
432
+ collectLocals(h.node.body, h.name, h.imports, h.node);
433
+ const localDecls = new Set(locals.map((l) => l.decl));
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]));
376
446
  const helpers = new Map();
377
- for (const fn of helperNodes) {
378
- const h = reporter.guard(() => analyzeHelper(fn.node, reporter, scopeFor(fn.imports), structs));
447
+ const generics = new Map();
448
+ const helperDecls = new Map();
449
+ for (const fn of [...helperNodes, ...locals.filter((l) => !inline.has(l.decl))]) {
450
+ helperDecls.set(fn.decl, fn.name);
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) {
453
+ reporter.guard(() => {
454
+ for (const tp of fn.node.typeParameters ?? [])
455
+ if (tp.default)
456
+ reporter.fail(tp, "type parameters of helpers take no default");
457
+ generics.set(fn.name, {
458
+ name: fn.name,
459
+ display: fn.display,
460
+ node: fn.node,
461
+ typeParams: (fn.node.typeParameters ?? []).map((t) => t.name.text),
462
+ fnParams,
463
+ imports: fn.imports,
464
+ });
465
+ });
466
+ continue;
467
+ }
468
+ const h = reporter.guard(() => analyzeHelper(fn.node, fn.name, fn.display, reporter, scopeFor(fn.imports), structs));
379
469
  if (h)
380
470
  helpers.set(h.name, h);
381
471
  }
382
- reporter.guard(() => checkRecursion(helpers, reporter));
472
+ const instances = new Map();
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("__");
476
+ const done = instances.get(key);
477
+ if (done)
478
+ return done;
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));
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
+ }
496
+ instances.set(key, h);
497
+ helpers.set(key, h);
498
+ return h;
499
+ };
500
+ reporter.guard(() => checkRecursion([...helperNodes, ...locals.filter((l) => !inline.has(l.decl))], helperDecls, checker, reporter));
383
501
  const accountStructs = new Set();
384
502
  const argStructs = new Set();
385
503
  const instructions = [];
@@ -390,12 +508,32 @@ export function analyze(entry) {
390
508
  instructions.push(ix);
391
509
  for (const a of ix.args)
392
510
  collectStructs(a.type, structs, dataEnums, argStructs);
511
+ for (const a of ix.args) {
512
+ const seen = new Set();
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;
516
+ for (const s of seen)
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`);
519
+ }
520
+ // Arguments are Borsh: a FixedVec's padded layout is account data only (a @layout borsh struct's FixedVec is a Borsh Vec).
521
+ for (const a of ix.args) {
522
+ const seen = new Set();
523
+ collectStructs(a.type, structs, dataEnums, seen);
524
+ for (const s of seen)
525
+ if (structs.get(s).layout !== "borsh" && structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
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`);
527
+ }
393
528
  // A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
394
529
  if (ix.returns?.kind === "struct")
395
530
  collectStructs(ix.returns, structs, dataEnums, argStructs);
396
531
  for (const a of ix.accounts)
397
- 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") {
398
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
+ }
399
537
  }
400
538
  checkSnakeDuplicates(instructions.map((ix) => ({ name: ix.node.name.text, node: ix.node.name })), "instructions", reporter);
401
539
  // emitCpi's event instruction (Anchor's EVENT_IX_TAG) is dispatched before every other instruction.
@@ -426,6 +564,9 @@ export function analyze(entry) {
426
564
  structs,
427
565
  instructions,
428
566
  helpers,
567
+ generics,
568
+ helperDecls,
569
+ closureDecls,
429
570
  errors,
430
571
  errorEnums,
431
572
  enums,
@@ -436,12 +577,14 @@ export function analyze(entry) {
436
577
  accountStructs,
437
578
  argStructs,
438
579
  fstrings,
580
+ fixedVecs,
439
581
  },
440
582
  sourceFile,
441
583
  files: programFiles(program, entryPath).map((f) => f.fileName),
442
584
  imports: entryImports,
443
585
  reporter,
444
- checker: program.getTypeChecker(),
586
+ checker,
587
+ instantiate,
445
588
  };
446
589
  }
447
590
  /** Every struct an argument type contains (transitively), for IDL layout decisions. */
@@ -479,7 +622,7 @@ export function resolveArgAggregate(node, scope, structs) {
479
622
  if (scalar)
480
623
  return scalar;
481
624
  if (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && !t.typeArguments?.length) {
482
- const s = structs.get(t.typeName.text);
625
+ const s = structs.get(structNameOf(t, scope));
483
626
  if (s && s.size !== undefined && !s.vec)
484
627
  return { kind: "struct", name: s.name };
485
628
  }
@@ -515,6 +658,11 @@ export function typesolName(node, imports) {
515
658
  export function resolveScalarType(node, scope) {
516
659
  if (node.kind === ts.SyntaxKind.BooleanKeyword)
517
660
  return { kind: "bool" };
661
+ if (scope.typeArgs && ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && !node.typeArguments) {
662
+ const t = scope.typeArgs.get(node.typeName.text);
663
+ if (t)
664
+ return t;
665
+ }
518
666
  const n = typesolName(node, scope.imports);
519
667
  if (n && INT_NAMES.includes(n))
520
668
  return { kind: "int", name: n };
@@ -557,6 +705,9 @@ export function resolveValueType(node, scope, allowVec = false) {
557
705
  return { kind: "dataEnumArg", name: node.typeName.text };
558
706
  if (allowVec && typesolName(node, scope.imports) === "Vec") {
559
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" } };
560
711
  const elem = elemNode && resolveScalarType(elemNode, scope);
561
712
  if (elem)
562
713
  return { kind: "vecArg", elem };
@@ -572,7 +723,7 @@ function numericLiteralArg(node) {
572
723
  * Resolve every exported interface. Field types may name other interfaces, so names are
573
724
  * collected first; sizes are computed after, rejecting cycles.
574
725
  */
575
- function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings) {
726
+ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs) {
576
727
  const names = new Set(items.map((d) => d.decl.name.text));
577
728
  const enumNames = new Set(aliases.map((a) => a.decl.name.text));
578
729
  const structs = new Map();
@@ -628,6 +779,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
628
779
  reporter.fail(targs[1], `array length must be 1..${MAX_ARRAY_LEN}`);
629
780
  return { kind: "array", elem, len };
630
781
  }
782
+ if (ext === "FixedVec") {
783
+ if (!targs[0])
784
+ reporter.fail(node, "FixedVec<T, N> needs an element type");
785
+ const elem = fieldType(targs[0]);
786
+ if (elem.kind === "string" || elem.kind === "vec")
787
+ reporter.fail(targs[0], "FixedVec elements must have a fixed size (use FixedString<N>)");
788
+ const cap = numericLiteralArg(targs[1]);
789
+ if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_ARRAY_LEN)
790
+ reporter.fail(node, `FixedVec<T, N> needs a literal capacity 1..${MAX_ARRAY_LEN}, e.g. FixedVec<u64, 16>`);
791
+ const t = { kind: "vec", elem, cap };
792
+ fixedVecs.set(fixedVecName(t), t);
793
+ return t;
794
+ }
631
795
  if (ext === "FixedString") {
632
796
  const cap = numericLiteralArg(targs[0]);
633
797
  if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_STRING_CAP)
@@ -637,7 +801,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
637
801
  }
638
802
  if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && names.has(node.typeName.text) && !targs.length)
639
803
  return { kind: "struct", name: node.typeName.text };
640
- reporter.fail(node, "field type must be an integer, boolean, Pubkey, FixedArray<T, N>, FixedString<N>, string (events only) or another struct");
804
+ reporter.fail(node, "field type must be an integer, boolean, Pubkey, FixedArray<T, N>, FixedString<N>, FixedVec<T, N>, Vec<T> (last), string (events only) or another struct");
641
805
  };
642
806
  for (const { decl, imports: fileImports } of items) {
643
807
  scope = scopeFor(fileImports);
@@ -658,12 +822,17 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
658
822
  if (problem)
659
823
  reporter.fail(m.name, problem);
660
824
  const type = fieldType(m.type);
661
- if (type.kind === "vec" && m !== decl.members[decl.members.length - 1])
662
- reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size)");
825
+ if (type.kind === "vec" && type.cap === undefined && m !== decl.members[decl.members.length - 1])
826
+ reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size); use FixedVec<T, N> for a list of at most N elsewhere");
663
827
  fields.push({ name, type });
664
828
  }
665
829
  checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
666
830
  const name = decl.name.text;
831
+ // `/** @layout borsh */`: stored as Anchor stores it.
832
+ const layoutTag = ts.getJSDocTags(decl).find((t) => t.tagName.text === "layout");
833
+ const layoutText = typeof layoutTag?.comment === "string" ? layoutTag.comment.trim() : undefined;
834
+ if (layoutTag && layoutText !== "borsh" && layoutText !== "fixed")
835
+ reporter.fail(layoutTag, "@layout is borsh (Anchor's account bytes) or fixed (the default)");
667
836
  structs.set(name, {
668
837
  name,
669
838
  fields,
@@ -671,6 +840,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
671
840
  accountDiscriminator: discriminator(`account:${name}`),
672
841
  eventDiscriminator: discriminator(`event:${name}`),
673
842
  node: decl,
843
+ layout: layoutText === "borsh" ? "borsh" : "fixed",
674
844
  });
675
845
  });
676
846
  }
@@ -766,8 +936,14 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
766
936
  return undefined;
767
937
  return 1 + e.payload;
768
938
  }
769
- case "vec":
770
- return 4; // the u32 length; elements live past the fixed part
939
+ case "vec": {
940
+ if (t.cap === undefined)
941
+ return 4; // the u32 length; elements live past the fixed part
942
+ const e = sizeOf(t.elem, at);
943
+ if (e === undefined)
944
+ reporter.fail(at, "FixedVec elements must have a fixed size");
945
+ return 4 + e * t.cap;
946
+ }
771
947
  case "string":
772
948
  return undefined;
773
949
  case "fstring":
@@ -799,14 +975,18 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
799
975
  state.set(s.name, "visiting");
800
976
  let total = 0;
801
977
  for (const f of s.fields) {
802
- 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);
803
983
  total = total === undefined || fsz === undefined ? undefined : total + fsz;
804
984
  }
805
985
  s.size = total;
806
986
  if (total !== undefined && total + 8 > MAX_ACCOUNT_SIZE)
807
987
  reporter.fail(s.node.name, `${s.name} is ${total + 8} bytes with its discriminator; Solana accounts are at most ${MAX_ACCOUNT_SIZE} bytes`);
808
988
  const last = s.fields.at(-1);
809
- if (last?.type.kind === "vec") {
989
+ if (last?.type.kind === "vec" && last.type.cap === undefined) {
810
990
  const elemSize = sizeOf(last.type.elem, s.node);
811
991
  if (elemSize === undefined)
812
992
  reporter.fail(s.node, "Vec elements must have a fixed size");
@@ -816,27 +996,118 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
816
996
  };
817
997
  for (const s of structs.values())
818
998
  reporter.guard(() => visit(s));
999
+ for (const s of structs.values())
1000
+ if (s.layout === "borsh")
1001
+ reporter.guard(() => checkBorshLayout(s, structs, dataEnums, reporter));
819
1002
  for (const e of dataEnums.values())
820
1003
  reporter.guard(() => sizeOf({ kind: "dataEnum", name: e.name }, e.node));
821
1004
  return structs;
822
1005
  }
823
- function analyzeHelper(fn, reporter, scope, structs) {
824
- if (!fn.name || !fn.body)
825
- reporter.fail(fn, "helper functions need a name and a body");
826
- if (fn.typeParameters?.length || fn.asteriskToken)
827
- reporter.fail(fn, "helper functions cannot be generic or generators");
1006
+ const isFunctionNode = (n) => ts.isFunctionDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isMethodDeclaration(n);
1007
+ /** Whether a local function reads a variable or parameter of a function around it (a closure). */
1008
+ function captures(l, localDecls, checker) {
1009
+ const fn = l.node;
1010
+ let found = false;
1011
+ const visit = (n) => {
1012
+ if (found)
1013
+ return;
1014
+ if (ts.isIdentifier(n)) {
1015
+ const sym = ts.isShorthandPropertyAssignment(n.parent) ? checker.getShorthandAssignmentValueSymbol(n.parent) : checker.getSymbolAtLocation(n);
1016
+ for (const d of sym?.declarations ?? []) {
1017
+ if (!(ts.isVariableDeclaration(d) || ts.isParameter(d) || ts.isBindingElement(d)) || localDecls.has(d))
1018
+ continue;
1019
+ if (d.getSourceFile() !== fn.getSourceFile() || (d.pos >= fn.pos && d.end <= fn.end))
1020
+ continue;
1021
+ for (let p = d.parent; p; p = p.parent)
1022
+ if (isFunctionNode(p))
1023
+ found = true;
1024
+ }
1025
+ }
1026
+ ts.forEachChild(n, visit);
1027
+ };
1028
+ if (fn.body)
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;
1047
+ }
1048
+ /**
1049
+ * A `@layout borsh` struct: its own fields may be FixedString (Borsh String), FixedVec (Borsh Vec) and Option; anything
1050
+ * nested must already be Borsh-identical (fixed-size scalars, Pubkeys, enums, arrays and structs of them).
1051
+ */
1052
+ function checkBorshLayout(s, structs, dataEnums, reporter) {
1053
+ const model = { structs };
1054
+ const fixed = (t) => borshIdentical(t, model);
1055
+ for (const f of s.fields) {
1056
+ const t = f.type;
1057
+ const at = s.node.members.find((m) => m.name && ts.isIdentifier(m.name) && m.name.text === f.name) ?? s.node;
1058
+ if (fixed(t) || t.kind === "fstring" || t.kind === "string")
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
+ }
1068
+ if (t.kind === "vec" && t.cap === undefined)
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)");
1070
+ if (t.kind === "vec" && (fixed(t.elem) || t.elem.kind === "fstring"))
1071
+ continue;
1072
+ if (t.kind === "option" && fixed(t.inner))
1073
+ continue;
1074
+ if (t.kind === "struct" && structs.get(t.name)?.layout === "borsh")
1075
+ reporter.fail(at, `${t.name} is @layout borsh itself: only account structs take a layout; nest a struct without one`);
1076
+ reporter.fail(at, `${typeName(t)} in a @layout borsh struct: its fields may be fixed-size values, FixedString<N>, FixedVec<T, N> (of those or of FixedString) and Option<T>; nested structs and lists hold fixed-size values only`);
1077
+ }
1078
+ for (const other of structs.values())
1079
+ for (const f of other.fields) {
1080
+ const inner = f.type.kind === "array" || f.type.kind === "vec" ? f.type.elem : f.type.kind === "option" ? f.type.inner : f.type;
1081
+ if (inner.kind === "struct" && inner.name === s.name)
1082
+ reporter.error(s.node.name, `${s.name} is @layout borsh, so it is account data only; ${other.name}.${f.name} cannot hold one`);
1083
+ }
1084
+ }
1085
+ function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new Set()) {
1086
+ if (!fn.body)
1087
+ reporter.fail(fn, "helper functions need a body");
1088
+ if (fn.asteriskToken)
1089
+ reporter.fail(fn, "helper functions cannot be generators");
1090
+ if ((ts.getModifiers(fn) ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword))
1091
+ reporter.fail(fn, "helper functions cannot be async");
828
1092
  if (!fn.type)
829
- reporter.fail(fn.name, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
830
- let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword ? undefined : resolveScalarType(fn.type, scope);
1093
+ reporter.fail(fn.name ?? fn, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
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);
831
1099
  if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns) {
832
- 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;
833
1102
  if (s && (s.size === undefined || s.vec))
834
1103
  reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be returned; return a struct of fixed size`);
835
1104
  if (!s)
836
- reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey or a struct");
1105
+ reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, string, a struct or a type parameter");
837
1106
  returns = { kind: "struct", name: s.name };
838
1107
  }
839
- const params = fn.parameters.map((p) => {
1108
+ const params = fn.parameters
1109
+ .filter((_, i) => !skip.has(i))
1110
+ .map((p) => {
840
1111
  if (!ts.isIdentifier(p.name) || !p.type || p.initializer || p.questionToken || p.dotDotDotToken)
841
1112
  reporter.fail(p, "parameters need a plain name and an explicit type");
842
1113
  // `p_<name>__n` carries a SignerSeeds count: "__" in a name could collide with it.
@@ -856,16 +1127,43 @@ function analyzeHelper(fn, reporter, scope, structs) {
856
1127
  reporter.fail(p.type, "helpers take accounts that exist: create Init/InitIfNeeded/Zeroed accounts in the instruction, then pass them as Mut<T>");
857
1128
  return { name: p.name.text, type: { kind: "account" }, account, node: p };
858
1129
  });
859
- return { name: fn.name.text, params, returns, node: fn, imports: scope.imports };
1130
+ return { name, display, params, returns, node: fn, statements: bodyStatements(fn, !returns), imports: scope.imports };
1131
+ }
1132
+ /** A function's statements; an arrow's expression body is a `return expr;` (or `expr;` when it returns void). */
1133
+ function bodyStatements(fn, isVoid) {
1134
+ const body = fn.body;
1135
+ if (ts.isBlock(body))
1136
+ return body.statements;
1137
+ const f = ts.factory;
1138
+ const stmt = isVoid ? f.createExpressionStatement(body) : f.createReturnStatement(body);
1139
+ ts.setTextRange(stmt, body);
1140
+ stmt.parent = fn;
1141
+ return [stmt];
1142
+ }
1143
+ /** The helper (or generic) a call's callee names: a local function by its declaration, others by name. */
1144
+ export function calleeName(callee, decls, checker) {
1145
+ let sym = checker.getSymbolAtLocation(callee);
1146
+ if (sym && sym.flags & ts.SymbolFlags.Alias)
1147
+ sym = checker.getAliasedSymbol(sym);
1148
+ for (const d of sym?.declarations ?? []) {
1149
+ const name = decls.get(d);
1150
+ if (name)
1151
+ return name;
1152
+ }
1153
+ return undefined;
860
1154
  }
861
1155
  /** Helpers may call each other, but not recursively: SBF has a small fixed call depth. */
862
- function checkRecursion(helpers, reporter) {
1156
+ function checkRecursion(sources, decls, checker, reporter) {
863
1157
  const calls = new Map();
864
- for (const h of helpers.values()) {
1158
+ const byName = new Map(sources.map((s) => [s.name, s]));
1159
+ for (const h of sources) {
865
1160
  const out = [];
866
1161
  const visit = (n) => {
867
- if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && helpers.has(n.expression.text))
868
- out.push(n.expression.text);
1162
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
1163
+ const c = calleeName(n.expression, decls, checker);
1164
+ if (c)
1165
+ out.push(c);
1166
+ }
869
1167
  ts.forEachChild(n, visit);
870
1168
  };
871
1169
  visit(h.node.body);
@@ -875,14 +1173,15 @@ function checkRecursion(helpers, reporter) {
875
1173
  const walk = (name, path) => {
876
1174
  if (state.get(name) === "done")
877
1175
  return;
1176
+ const src = byName.get(name);
878
1177
  if (state.get(name) === "visiting")
879
- reporter.fail(helpers.get(name).node.name, `recursion is not supported: ${[...path, name].join(" -> ")}`);
1178
+ reporter.fail(src.node.name ?? src.node, `recursion is not supported: ${[...path, name].map((n) => byName.get(n).display).join(" -> ")}`);
880
1179
  state.set(name, "visiting");
881
1180
  for (const c of calls.get(name) ?? [])
882
1181
  walk(c, [...path, name]);
883
1182
  state.set(name, "done");
884
1183
  };
885
- for (const name of helpers.keys())
1184
+ for (const name of byName.keys())
886
1185
  walk(name, []);
887
1186
  }
888
1187
  function analyzeInstruction(fn, reporter, scope, structs) {
@@ -895,7 +1194,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
895
1194
  reporter.fail(fn, "instructions cannot be async or generators");
896
1195
  let returns;
897
1196
  if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword) {
898
- returns = resolveScalarType(fn.type, scope);
1197
+ returns = fn.type.kind === ts.SyntaxKind.StringKeyword ? { kind: "string" } : resolveScalarType(fn.type, scope);
899
1198
  const s = !returns && ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments
900
1199
  ? structs.get(fn.type.typeName.text)
901
1200
  : undefined;
@@ -908,7 +1207,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
908
1207
  returns = { kind: "struct", name: s.name };
909
1208
  }
910
1209
  if (!returns)
911
- 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)");
912
1211
  }
913
1212
  const accounts = [];
914
1213
  const args = [];
@@ -948,7 +1247,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
948
1247
  const inner = unwrapConstraints(typeNode, reporter, imports, constraints);
949
1248
  const kind = resolveAccountKind(inner, reporter, imports, structs);
950
1249
  if (optional &&
951
- (["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)))
952
1252
  reporter.fail(p, "Init, InitIfNeeded, Zeroed, PythPriceUpdate, EventAuthority, SelfProgram and ProgramData accounts cannot be optional");
953
1253
  accounts.push({ name: p.name.text, index: accounts.length, kind, node: p, constraints, ...(optional ? { optional } : {}) });
954
1254
  });
@@ -961,10 +1261,11 @@ function analyzeInstruction(fn, reporter, scope, structs) {
961
1261
  // Constraints refer to other parameters by name: check them now that all are known.
962
1262
  for (const a of accounts) {
963
1263
  const c = a.constraints;
964
- if (a.kind.kind === "token" && a.kind.init && !c.ata)
965
- reporter.fail(a.node, 'Init<TokenAccount> / InitIfNeeded<TokenAccount> create associated token accounts: wrap them in Ata<…, "owner", "mint">');
966
- 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))
967
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`);
968
1269
  if (!c.hasOne.length && !c.seeds && !c.closeTo && !c.ata)
969
1270
  continue;
970
1271
  const struct = a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded" || a.kind.kind === "zeroed" ? a.kind.struct : undefined;
@@ -1038,7 +1339,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1038
1339
  const pushes = (() => {
1039
1340
  let found = false;
1040
1341
  const visit = (n) => {
1041
- if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "push")
1342
+ // Vec.push(value, payer) grows the account; FixedVec.push(value) does not.
1343
+ if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "push" && n.arguments.length === 2)
1042
1344
  found = true;
1043
1345
  ts.forEachChild(n, visit);
1044
1346
  };
@@ -1155,6 +1457,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1155
1457
  // Any account (helpers that accept every kind, e.g. generated CPI helpers).
1156
1458
  case "AccountMeta":
1157
1459
  return { kind: "info", mut: false };
1460
+ case "SystemAccount":
1461
+ return { kind: "info", mut: false, systemOwned: true };
1158
1462
  case "TokenAccount":
1159
1463
  return { kind: "token", mut: false };
1160
1464
  case "Mint":
@@ -1212,9 +1516,11 @@ function resolveAccountKind(node, reporter, imports, structs) {
1212
1516
  case "Init": {
1213
1517
  if (typesolName(inner(), imports) === "TokenAccount")
1214
1518
  return { kind: "token", mut: true, init: "create" };
1519
+ if (typesolName(inner(), imports) === "Mint")
1520
+ return { kind: "mint", mut: true, init: "create" };
1215
1521
  const s = structOf(inner());
1216
1522
  if (!s)
1217
- 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");
1218
1524
  if (s.size + 8 > MAX_CPI_ALLOCATION)
1219
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}>`);
1220
1526
  return { kind: "init", struct: s };
@@ -1222,6 +1528,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1222
1528
  case "InitIfNeeded": {
1223
1529
  if (typesolName(inner(), imports) === "TokenAccount")
1224
1530
  return { kind: "token", mut: true, init: "ifNeeded" };
1531
+ if (typesolName(inner(), imports) === "Mint")
1532
+ return { kind: "mint", mut: true, init: "ifNeeded" };
1225
1533
  const s = structOf(inner());
1226
1534
  if (!s)
1227
1535
  reporter.fail(node, "InitIfNeeded<T> needs a struct declared in this program");
@@ -1235,6 +1543,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1235
1543
  reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
1236
1544
  if (s.vec)
1237
1545
  reporter.fail(node, `${s.name} ends in a Vec; Zeroed<T> is for fixed-size (large) accounts`);
1546
+ if (s.layout === "borsh")
1547
+ reporter.fail(node, `${s.name} is @layout borsh; Zeroed<T> reads large accounts in place, so it takes fixed layouts only`);
1238
1548
  return { kind: "zeroed", struct: s };
1239
1549
  }
1240
1550
  case "Mut": {
@@ -1247,6 +1557,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1247
1557
  return { kind: "signer", mut: true };
1248
1558
  if (innerName === "AccountInfo" || innerName === "AccountMeta")
1249
1559
  return { kind: "info", mut: true };
1560
+ if (innerName === "SystemAccount")
1561
+ return { kind: "info", mut: true, systemOwned: true };
1250
1562
  if (innerName === "TokenAccount")
1251
1563
  return { kind: "token", mut: true };
1252
1564
  if (innerName === "Mint")