@typesol/cli 0.15.0 → 0.17.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +11 -7
  3. package/dist/cli.js +147 -20
  4. package/dist/clientgen.js +37 -0
  5. package/dist/compiler/analyze.d.ts +11 -1
  6. package/dist/compiler/analyze.js +307 -37
  7. package/dist/compiler/emit/base.d.ts +7 -0
  8. package/dist/compiler/emit/base.js +62 -2
  9. package/dist/compiler/emit/borsh.d.ts +13 -0
  10. package/dist/compiler/emit/borsh.js +232 -0
  11. package/dist/compiler/emit/calls.js +50 -15
  12. package/dist/compiler/emit/cpi.d.ts +32 -8
  13. package/dist/compiler/emit/cpi.js +706 -25
  14. package/dist/compiler/emit/expressions.d.ts +11 -0
  15. package/dist/compiler/emit/expressions.js +126 -4
  16. package/dist/compiler/emit/function.js +45 -12
  17. package/dist/compiler/emit/program.js +24 -6
  18. package/dist/compiler/emit/shared.d.ts +12 -0
  19. package/dist/compiler/emit/shared.js +22 -4
  20. package/dist/compiler/emit/statements.d.ts +13 -0
  21. package/dist/compiler/emit/statements.js +141 -7
  22. package/dist/compiler/emit/storage.d.ts +6 -1
  23. package/dist/compiler/emit/storage.js +82 -17
  24. package/dist/compiler/idl.js +61 -18
  25. package/dist/compiler/model.d.ts +56 -4
  26. package/dist/compiler/model.js +8 -2
  27. package/dist/compiler/toolchain.d.ts +2 -0
  28. package/dist/compiler/toolchain.js +27 -1
  29. package/dist/onchain.d.ts +30 -0
  30. package/dist/onchain.js +53 -0
  31. package/dist/portgen.d.ts +87 -0
  32. package/dist/portgen.js +367 -0
  33. package/dist/templates.js +49 -154
  34. package/dist/testing.d.ts +66 -0
  35. package/dist/testing.js +337 -0
  36. package/dist/workspace.js +11 -1
  37. package/docs/architecture.md +3 -0
  38. package/docs/cli.md +16 -2
  39. package/docs/errors.md +3 -2
  40. package/docs/language.md +19 -14
  41. package/docs/mainnet.md +5 -4
  42. package/docs/migrate-from-anchor.md +113 -0
  43. package/docs/test-your-program.md +52 -0
  44. package/docs/testing.md +1 -1
  45. package/docs/troubleshooting.md +7 -0
  46. package/lib/typesol.d.ts +325 -13
  47. package/package.json +19 -1
  48. package/runtime/typesol_rt.h +54 -9
  49. package/ts-plugin/index.js +112 -0
  50. package/ts-plugin/package.json +4 -0
@@ -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"]);
@@ -17,7 +18,7 @@ const C_RESERVED = new Set([
17
18
  ..."NULL SUCCESS HEAP_LENGTH HEAP_START_ADDRESS MAX_PERMITTED_DATA_INCREASE MAX_RETURN_DATA MAX_SEED_LENGTH_EXCEEDED INVALID_SEEDS SIZE_PUBKEY memcpy memset strlen static_assert offsetof sol_log_64".split(" "),
18
19
  ]);
19
20
  /** Why a name cannot be used as a C member, or undefined when it can. */
20
- function cNameProblem(name) {
21
+ export function cNameProblem(name) {
21
22
  if (name.includes("__") || name.startsWith("_"))
22
23
  return `"${name}": names starting with "_" or containing "__" are reserved for generated code`;
23
24
  if (name.includes("$"))
@@ -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",
@@ -94,6 +96,8 @@ const MAX_ARRAY_LEN = 1_000_000;
94
96
  const MAX_STRING_CAP = 10_240;
95
97
  /** Solana's account size limit, and what a program can allocate itself through a CPI (Init<T>). */
96
98
  export const MAX_ACCOUNT_SIZE = 10 * 1024 * 1024;
99
+ /** sol_set_return_data's limit. */
100
+ export const MAX_RETURN_DATA = 1024;
97
101
  export const MAX_CPI_ALLOCATION = 10 * 1024;
98
102
  export function snakeCase(name) {
99
103
  return name
@@ -223,7 +227,7 @@ export function analyze(entry) {
223
227
  const isExported = (n) => ts.canHaveModifiers(n) && (ts.getModifiers(n) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
224
228
  const declare = (name, file) => {
225
229
  // These prefixes name the IDL types TypeSOL generates for FixedString<N>, Option and data enums in account data.
226
- if (/^(FixedString\d|FixedOption_|FixedEnum_)/.test(name.text))
230
+ if (/^(FixedString\d|FixedVec\d|FixedOption_|FixedEnum_)/.test(name.text))
227
231
  reporter.fail(name, `"${name.text}" is reserved for IDL types TypeSOL generates; pick another name`);
228
232
  const prev = declared.get(name.text);
229
233
  if (prev)
@@ -311,7 +315,8 @@ export function analyze(entry) {
311
315
  else {
312
316
  if (stmt.name)
313
317
  declare(stmt.name, file);
314
- helperNodes.push({ node: stmt, imports });
318
+ if (stmt.name)
319
+ helperNodes.push({ node: stmt, name: stmt.name.text, display: stmt.name.text, decl: stmt, imports });
315
320
  }
316
321
  }
317
322
  else if (ts.isVariableStatement(stmt)) {
@@ -322,6 +327,14 @@ export function analyze(entry) {
322
327
  if (!ts.isIdentifier(d.name) || !d.initializer)
323
328
  reporter.fail(d, "declare const NAME = literal");
324
329
  const init = d.initializer;
330
+ // `const f = (…): T => …` / `const f = function (…) {…}`: a helper.
331
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
332
+ if (isEntry && isExported(stmt))
333
+ reporter.fail(d.name, "instructions are function declarations: export function name(…)");
334
+ declare(d.name, file);
335
+ helperNodes.push({ node: init, name: d.name.text, display: d.name.text, decl: d, imports });
336
+ continue;
337
+ }
325
338
  let value;
326
339
  if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && imports.get(init.expression.text) === "pubkey") {
327
340
  const arg = init.arguments[0];
@@ -356,6 +369,7 @@ export function analyze(entry) {
356
369
  }
357
370
  }
358
371
  const fstrings = new Set();
372
+ const fixedVecs = new Map();
359
373
  const scopeFor = (imports) => ({ imports, enums, options, dataEnums });
360
374
  for (const c of pendingConstTypes) {
361
375
  reporter.guard(() => {
@@ -370,14 +384,87 @@ export function analyze(entry) {
370
384
  c.def.type = t;
371
385
  });
372
386
  }
373
- const structs = analyzeStructs(interfaces, aliases, dataEnums, reporter, scopeFor, fstrings);
387
+ const structs = analyzeStructs(interfaces, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs);
388
+ const checker = program.getTypeChecker();
389
+ // Local `const f = (…) => …` in any function body (and in those local functions): lifted to helpers.
390
+ const locals = [];
391
+ const taken = new Set();
392
+ const collectLocals = (body, outer, imports, fn) => {
393
+ const generic = !!fn.typeParameters?.length;
394
+ const visit = (n) => {
395
+ if (ts.isVariableDeclaration(n) && n.initializer && (ts.isArrowFunction(n.initializer) || ts.isFunctionExpression(n.initializer))) {
396
+ reporter.guard(() => {
397
+ if (!ts.isIdentifier(n.name))
398
+ reporter.fail(n.name, "name local functions: const f = (…) => …");
399
+ if (generic)
400
+ reporter.fail(n, "local functions inside a generic helper are not supported; declare this one at the top level");
401
+ if (!(n.parent.flags & ts.NodeFlags.Const) || !ts.isVariableStatement(n.parent.parent))
402
+ reporter.fail(n, "declare local functions with const: const f = (…) => …");
403
+ if (n.name.text.includes("__") || n.name.text.includes("$"))
404
+ reporter.fail(n.name, `"${n.name.text}": "__" and "$" are reserved in names`);
405
+ let name = `${outer}__${n.name.text}`;
406
+ for (let i = 2; taken.has(name); i++)
407
+ name = `${outer}__${n.name.text}__${i}`;
408
+ taken.add(name);
409
+ const src = { node: n.initializer, name, display: n.name.text, decl: n, imports, outer: fn };
410
+ locals.push(src);
411
+ if (src.node.body)
412
+ collectLocals(src.node.body, name, imports, src.node);
413
+ });
414
+ return;
415
+ }
416
+ ts.forEachChild(n, visit);
417
+ };
418
+ visit(body);
419
+ };
420
+ for (const f of functions)
421
+ if (f.node.body && f.node.name)
422
+ collectLocals(f.node.body, f.node.name.text, f.imports, f.node);
423
+ for (const h of [...helperNodes])
424
+ if (h.node.body)
425
+ collectLocals(h.node.body, h.name, h.imports, h.node);
426
+ const localDecls = new Set(locals.map((l) => l.decl));
427
+ for (const l of locals)
428
+ reporter.guard(() => checkCaptures(l, localDecls, checker, reporter));
374
429
  const helpers = new Map();
375
- for (const fn of helperNodes) {
376
- const h = reporter.guard(() => analyzeHelper(fn.node, reporter, scopeFor(fn.imports), structs));
430
+ const generics = new Map();
431
+ const helperDecls = new Map();
432
+ for (const fn of [...helperNodes, ...locals]) {
433
+ helperDecls.set(fn.decl, fn.name);
434
+ if (fn.node.typeParameters?.length) {
435
+ reporter.guard(() => {
436
+ for (const tp of fn.node.typeParameters)
437
+ if (tp.default)
438
+ reporter.fail(tp, "type parameters of helpers take no default");
439
+ generics.set(fn.name, {
440
+ name: fn.name,
441
+ display: fn.display,
442
+ node: fn.node,
443
+ typeParams: fn.node.typeParameters.map((t) => t.name.text),
444
+ imports: fn.imports,
445
+ });
446
+ });
447
+ continue;
448
+ }
449
+ const h = reporter.guard(() => analyzeHelper(fn.node, fn.name, fn.display, reporter, scopeFor(fn.imports), structs));
377
450
  if (h)
378
451
  helpers.set(h.name, h);
379
452
  }
380
- reporter.guard(() => checkRecursion(helpers, reporter));
453
+ const instances = new Map();
454
+ const instantiate = (g, typeArgs) => {
455
+ const key = `${g.name}__${typeArgs.map(scalarKey).join("_")}`;
456
+ const done = instances.get(key);
457
+ if (done)
458
+ 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);
462
+ h.typeArgs = bound;
463
+ instances.set(key, h);
464
+ helpers.set(key, h);
465
+ return h;
466
+ };
467
+ reporter.guard(() => checkRecursion([...helperNodes, ...locals], helperDecls, checker, reporter));
381
468
  const accountStructs = new Set();
382
469
  const argStructs = new Set();
383
470
  const instructions = [];
@@ -388,6 +475,26 @@ export function analyze(entry) {
388
475
  instructions.push(ix);
389
476
  for (const a of ix.args)
390
477
  collectStructs(a.type, structs, dataEnums, argStructs);
478
+ for (const a of ix.args) {
479
+ const seen = new Set();
480
+ collectStructs(a.type, structs, dataEnums, seen);
481
+ 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`);
484
+ }
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.
488
+ for (const a of ix.args) {
489
+ const seen = new Set();
490
+ collectStructs(a.type, structs, dataEnums, seen);
491
+ for (const s of seen)
492
+ if (structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
493
+ 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
+ }
495
+ // A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
496
+ if (ix.returns?.kind === "struct")
497
+ collectStructs(ix.returns, structs, dataEnums, argStructs);
391
498
  for (const a of ix.accounts)
392
499
  if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded")
393
500
  accountStructs.add(a.kind.struct.name);
@@ -421,6 +528,8 @@ export function analyze(entry) {
421
528
  structs,
422
529
  instructions,
423
530
  helpers,
531
+ generics,
532
+ helperDecls,
424
533
  errors,
425
534
  errorEnums,
426
535
  enums,
@@ -431,12 +540,14 @@ export function analyze(entry) {
431
540
  accountStructs,
432
541
  argStructs,
433
542
  fstrings,
543
+ fixedVecs,
434
544
  },
435
545
  sourceFile,
436
546
  files: programFiles(program, entryPath).map((f) => f.fileName),
437
547
  imports: entryImports,
438
548
  reporter,
439
- checker: program.getTypeChecker(),
549
+ checker,
550
+ instantiate,
440
551
  };
441
552
  }
442
553
  /** Every struct an argument type contains (transitively), for IDL layout decisions. */
@@ -510,6 +621,11 @@ export function typesolName(node, imports) {
510
621
  export function resolveScalarType(node, scope) {
511
622
  if (node.kind === ts.SyntaxKind.BooleanKeyword)
512
623
  return { kind: "bool" };
624
+ if (scope.typeArgs && ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && !node.typeArguments) {
625
+ const t = scope.typeArgs.get(node.typeName.text);
626
+ if (t)
627
+ return t;
628
+ }
513
629
  const n = typesolName(node, scope.imports);
514
630
  if (n && INT_NAMES.includes(n))
515
631
  return { kind: "int", name: n };
@@ -567,7 +683,7 @@ function numericLiteralArg(node) {
567
683
  * Resolve every exported interface. Field types may name other interfaces, so names are
568
684
  * collected first; sizes are computed after, rejecting cycles.
569
685
  */
570
- function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings) {
686
+ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs) {
571
687
  const names = new Set(items.map((d) => d.decl.name.text));
572
688
  const enumNames = new Set(aliases.map((a) => a.decl.name.text));
573
689
  const structs = new Map();
@@ -623,6 +739,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
623
739
  reporter.fail(targs[1], `array length must be 1..${MAX_ARRAY_LEN}`);
624
740
  return { kind: "array", elem, len };
625
741
  }
742
+ if (ext === "FixedVec") {
743
+ if (!targs[0])
744
+ reporter.fail(node, "FixedVec<T, N> needs an element type");
745
+ const elem = fieldType(targs[0]);
746
+ if (elem.kind === "string" || elem.kind === "vec")
747
+ reporter.fail(targs[0], "FixedVec elements must have a fixed size (use FixedString<N>)");
748
+ const cap = numericLiteralArg(targs[1]);
749
+ if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_ARRAY_LEN)
750
+ reporter.fail(node, `FixedVec<T, N> needs a literal capacity 1..${MAX_ARRAY_LEN}, e.g. FixedVec<u64, 16>`);
751
+ const t = { kind: "vec", elem, cap };
752
+ fixedVecs.set(fixedVecName(t), t);
753
+ return t;
754
+ }
626
755
  if (ext === "FixedString") {
627
756
  const cap = numericLiteralArg(targs[0]);
628
757
  if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_STRING_CAP)
@@ -632,7 +761,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
632
761
  }
633
762
  if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && names.has(node.typeName.text) && !targs.length)
634
763
  return { kind: "struct", name: node.typeName.text };
635
- reporter.fail(node, "field type must be an integer, boolean, Pubkey, FixedArray<T, N>, FixedString<N>, string (events only) or another struct");
764
+ 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");
636
765
  };
637
766
  for (const { decl, imports: fileImports } of items) {
638
767
  scope = scopeFor(fileImports);
@@ -653,12 +782,17 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
653
782
  if (problem)
654
783
  reporter.fail(m.name, problem);
655
784
  const type = fieldType(m.type);
656
- if (type.kind === "vec" && m !== decl.members[decl.members.length - 1])
657
- reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size)");
785
+ if (type.kind === "vec" && type.cap === undefined && m !== decl.members[decl.members.length - 1])
786
+ 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");
658
787
  fields.push({ name, type });
659
788
  }
660
789
  checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
661
790
  const name = decl.name.text;
791
+ // `/** @layout borsh */`: stored as Anchor stores it.
792
+ const layoutTag = ts.getJSDocTags(decl).find((t) => t.tagName.text === "layout");
793
+ const layoutText = typeof layoutTag?.comment === "string" ? layoutTag.comment.trim() : undefined;
794
+ if (layoutTag && layoutText !== "borsh" && layoutText !== "fixed")
795
+ reporter.fail(layoutTag, "@layout is borsh (Anchor's account bytes) or fixed (the default)");
662
796
  structs.set(name, {
663
797
  name,
664
798
  fields,
@@ -666,6 +800,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
666
800
  accountDiscriminator: discriminator(`account:${name}`),
667
801
  eventDiscriminator: discriminator(`event:${name}`),
668
802
  node: decl,
803
+ layout: layoutText === "borsh" ? "borsh" : "fixed",
669
804
  });
670
805
  });
671
806
  }
@@ -696,8 +831,8 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
696
831
  if (problem)
697
832
  reporter.fail(p.name, problem);
698
833
  const t = fieldType(p.type);
699
- if (t.kind === "string" || t.kind === "vec")
700
- reporter.fail(p.type, "variant fields must have a fixed size (use FixedString<N>)");
834
+ if (t.kind === "vec")
835
+ reporter.fail(p.type, "variant fields must have a fixed size (a Vec cannot be one; use FixedArray<T, N>)");
701
836
  fields.push({ name: p.name.text, type: t });
702
837
  }
703
838
  if (!vname || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(vname))
@@ -734,15 +869,41 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
734
869
  reporter.fail(at, `data enum ${e.name} contains itself`);
735
870
  if (state.get(`enum:${e.name}`) !== "done") {
736
871
  state.set(`enum:${e.name}`, "visiting");
872
+ // A string field is a TS_Str view (16 bytes) in the C payload and makes the enum variable-size.
873
+ const fieldSize = (f) => {
874
+ if (f.kind === "string") {
875
+ e.variable = true;
876
+ return 16;
877
+ }
878
+ if (f.kind === "dataEnum") {
879
+ const inner = dataEnums.get(f.name);
880
+ const size = sizeOf(f, e.node);
881
+ if (inner?.variable) {
882
+ e.variable = true;
883
+ return 1 + inner.payload;
884
+ }
885
+ return size;
886
+ }
887
+ return sizeOf(f, e.node);
888
+ };
737
889
  for (const v of e.variants)
738
- v.size = v.fields.reduce((sum, f) => sum + sizeOf(f.type, e.node), 0);
890
+ v.size = v.fields.reduce((sum, f) => sum + fieldSize(f.type), 0);
739
891
  e.payload = Math.max(0, ...e.variants.map((v) => v.size));
740
892
  state.set(`enum:${e.name}`, "done");
741
893
  }
894
+ // Variable enums have no fixed layout: a struct holding one is event-only, like one holding a string.
895
+ if (e.variable)
896
+ return undefined;
742
897
  return 1 + e.payload;
743
898
  }
744
- case "vec":
745
- return 4; // the u32 length; elements live past the fixed part
899
+ case "vec": {
900
+ if (t.cap === undefined)
901
+ return 4; // the u32 length; elements live past the fixed part
902
+ const e = sizeOf(t.elem, at);
903
+ if (e === undefined)
904
+ reporter.fail(at, "FixedVec elements must have a fixed size");
905
+ return 4 + e * t.cap;
906
+ }
746
907
  case "string":
747
908
  return undefined;
748
909
  case "fstring":
@@ -781,7 +942,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
781
942
  if (total !== undefined && total + 8 > MAX_ACCOUNT_SIZE)
782
943
  reporter.fail(s.node.name, `${s.name} is ${total + 8} bytes with its discriminator; Solana accounts are at most ${MAX_ACCOUNT_SIZE} bytes`);
783
944
  const last = s.fields.at(-1);
784
- if (last?.type.kind === "vec") {
945
+ if (last?.type.kind === "vec" && last.type.cap === undefined) {
785
946
  const elemSize = sizeOf(last.type.elem, s.node);
786
947
  if (elemSize === undefined)
787
948
  reporter.fail(s.node, "Vec elements must have a fixed size");
@@ -791,20 +952,87 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
791
952
  };
792
953
  for (const s of structs.values())
793
954
  reporter.guard(() => visit(s));
955
+ for (const s of structs.values())
956
+ if (s.layout === "borsh")
957
+ reporter.guard(() => checkBorshLayout(s, structs, reporter));
794
958
  for (const e of dataEnums.values())
795
959
  reporter.guard(() => sizeOf({ kind: "dataEnum", name: e.name }, e.node));
796
960
  return structs;
797
961
  }
798
- function analyzeHelper(fn, reporter, scope, structs) {
799
- if (!fn.name || !fn.body)
800
- reporter.fail(fn, "helper functions need a name and a body");
801
- if (fn.typeParameters?.length || fn.asteriskToken)
802
- reporter.fail(fn, "helper functions cannot be generic or generators");
962
+ 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) {
968
+ const fn = l.node;
969
+ const visit = (n) => {
970
+ if (ts.isIdentifier(n)) {
971
+ const sym = ts.isShorthandPropertyAssignment(n.parent) ? checker.getShorthandAssignmentValueSymbol(n.parent) : checker.getSymbolAtLocation(n);
972
+ for (const d of sym?.declarations ?? []) {
973
+ if (!(ts.isVariableDeclaration(d) || ts.isParameter(d) || ts.isBindingElement(d)) || localDecls.has(d))
974
+ continue;
975
+ if (d.getSourceFile() !== fn.getSourceFile() || (d.pos >= fn.pos && d.end <= fn.end))
976
+ continue;
977
+ // Declared in some function around this one: a captured variable.
978
+ 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
+ }
982
+ }
983
+ }
984
+ ts.forEachChild(n, visit);
985
+ };
986
+ if (fn.body)
987
+ visit(fn.body);
988
+ }
989
+ /**
990
+ * A `@layout borsh` struct: its own fields may be FixedString (Borsh String), FixedVec (Borsh Vec) and Option; anything
991
+ * nested must already be Borsh-identical (fixed-size scalars, Pubkeys, enums, arrays and structs of them).
992
+ */
993
+ function checkBorshLayout(s, structs, reporter) {
994
+ const model = { structs };
995
+ const fixed = (t) => borshIdentical(t, model);
996
+ for (const f of s.fields) {
997
+ const t = f.type;
998
+ 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")
1000
+ continue;
1001
+ if (t.kind === "vec" && t.cap === undefined)
1002
+ 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
+ if (t.kind === "vec" && (fixed(t.elem) || t.elem.kind === "fstring"))
1004
+ continue;
1005
+ if (t.kind === "option" && fixed(t.inner))
1006
+ continue;
1007
+ if (t.kind === "struct" && structs.get(t.name)?.layout === "borsh")
1008
+ reporter.fail(at, `${t.name} is @layout borsh itself: only account structs take a layout; nest a struct without one`);
1009
+ 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`);
1010
+ }
1011
+ for (const other of structs.values())
1012
+ for (const f of other.fields) {
1013
+ const inner = f.type.kind === "array" || f.type.kind === "vec" ? f.type.elem : f.type.kind === "option" ? f.type.inner : f.type;
1014
+ if (inner.kind === "struct" && inner.name === s.name)
1015
+ reporter.error(s.node.name, `${s.name} is @layout borsh, so it is account data only; ${other.name}.${f.name} cannot hold one`);
1016
+ }
1017
+ }
1018
+ function analyzeHelper(fn, name, display, reporter, scope, structs) {
1019
+ if (!fn.body)
1020
+ reporter.fail(fn, "helper functions need a body");
1021
+ if (fn.asteriskToken)
1022
+ reporter.fail(fn, "helper functions cannot be generators");
1023
+ if ((ts.getModifiers(fn) ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword))
1024
+ reporter.fail(fn, "helper functions cannot be async");
803
1025
  if (!fn.type)
804
- reporter.fail(fn.name, "helper functions need an explicit return type (void, an integer, boolean or Pubkey)");
805
- const returns = fn.type.kind === ts.SyntaxKind.VoidKeyword ? undefined : resolveScalarType(fn.type, scope);
806
- if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns)
807
- reporter.fail(fn.type, "helpers return void, an integer, boolean or Pubkey");
1026
+ 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);
1028
+ 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;
1030
+ if (s && (s.size === undefined || s.vec))
1031
+ reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be returned; return a struct of fixed size`);
1032
+ if (!s)
1033
+ reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, a struct or a type parameter");
1034
+ returns = { kind: "struct", name: s.name };
1035
+ }
808
1036
  const params = fn.parameters.map((p) => {
809
1037
  if (!ts.isIdentifier(p.name) || !p.type || p.initializer || p.questionToken || p.dotDotDotToken)
810
1038
  reporter.fail(p, "parameters need a plain name and an explicit type");
@@ -825,16 +1053,43 @@ function analyzeHelper(fn, reporter, scope, structs) {
825
1053
  reporter.fail(p.type, "helpers take accounts that exist: create Init/InitIfNeeded/Zeroed accounts in the instruction, then pass them as Mut<T>");
826
1054
  return { name: p.name.text, type: { kind: "account" }, account, node: p };
827
1055
  });
828
- return { name: fn.name.text, params, returns, node: fn, imports: scope.imports };
1056
+ return { name, display, params, returns, node: fn, statements: bodyStatements(fn, !returns), imports: scope.imports };
1057
+ }
1058
+ /** A function's statements; an arrow's expression body is a `return expr;` (or `expr;` when it returns void). */
1059
+ function bodyStatements(fn, isVoid) {
1060
+ const body = fn.body;
1061
+ if (ts.isBlock(body))
1062
+ return body.statements;
1063
+ const f = ts.factory;
1064
+ const stmt = isVoid ? f.createExpressionStatement(body) : f.createReturnStatement(body);
1065
+ ts.setTextRange(stmt, body);
1066
+ stmt.parent = fn;
1067
+ return [stmt];
1068
+ }
1069
+ /** The helper (or generic) a call's callee names: a local function by its declaration, others by name. */
1070
+ export function calleeName(callee, decls, checker) {
1071
+ let sym = checker.getSymbolAtLocation(callee);
1072
+ if (sym && sym.flags & ts.SymbolFlags.Alias)
1073
+ sym = checker.getAliasedSymbol(sym);
1074
+ for (const d of sym?.declarations ?? []) {
1075
+ const name = decls.get(d);
1076
+ if (name)
1077
+ return name;
1078
+ }
1079
+ return undefined;
829
1080
  }
830
1081
  /** Helpers may call each other, but not recursively: SBF has a small fixed call depth. */
831
- function checkRecursion(helpers, reporter) {
1082
+ function checkRecursion(sources, decls, checker, reporter) {
832
1083
  const calls = new Map();
833
- for (const h of helpers.values()) {
1084
+ const byName = new Map(sources.map((s) => [s.name, s]));
1085
+ for (const h of sources) {
834
1086
  const out = [];
835
1087
  const visit = (n) => {
836
- if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && helpers.has(n.expression.text))
837
- out.push(n.expression.text);
1088
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
1089
+ const c = calleeName(n.expression, decls, checker);
1090
+ if (c)
1091
+ out.push(c);
1092
+ }
838
1093
  ts.forEachChild(n, visit);
839
1094
  };
840
1095
  visit(h.node.body);
@@ -844,14 +1099,15 @@ function checkRecursion(helpers, reporter) {
844
1099
  const walk = (name, path) => {
845
1100
  if (state.get(name) === "done")
846
1101
  return;
1102
+ const src = byName.get(name);
847
1103
  if (state.get(name) === "visiting")
848
- reporter.fail(helpers.get(name).node.name, `recursion is not supported: ${[...path, name].join(" -> ")}`);
1104
+ reporter.fail(src.node.name ?? src.node, `recursion is not supported: ${[...path, name].map((n) => byName.get(n).display).join(" -> ")}`);
849
1105
  state.set(name, "visiting");
850
1106
  for (const c of calls.get(name) ?? [])
851
1107
  walk(c, [...path, name]);
852
1108
  state.set(name, "done");
853
1109
  };
854
- for (const name of helpers.keys())
1110
+ for (const name of byName.keys())
855
1111
  walk(name, []);
856
1112
  }
857
1113
  function analyzeInstruction(fn, reporter, scope, structs) {
@@ -865,8 +1121,19 @@ function analyzeInstruction(fn, reporter, scope, structs) {
865
1121
  let returns;
866
1122
  if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword) {
867
1123
  returns = resolveScalarType(fn.type, scope);
1124
+ const s = !returns && ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments
1125
+ ? structs.get(fn.type.typeName.text)
1126
+ : undefined;
1127
+ if (s) {
1128
+ if (s.size === undefined || s.vec)
1129
+ reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be return data`);
1130
+ // The runtime caps return data at 1024 bytes.
1131
+ if (s.size > MAX_RETURN_DATA)
1132
+ reporter.fail(fn.type, `${s.name} is ${s.size} bytes; return data holds at most ${MAX_RETURN_DATA}`);
1133
+ returns = { kind: "struct", name: s.name };
1134
+ }
868
1135
  if (!returns)
869
- reporter.fail(fn.type, "instructions return void or one integer, boolean or Pubkey (sent as return data)");
1136
+ reporter.fail(fn.type, "instructions return void, one integer, boolean or Pubkey, or a fixed-size struct (sent as return data)");
870
1137
  }
871
1138
  const accounts = [];
872
1139
  const args = [];
@@ -996,7 +1263,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
996
1263
  const pushes = (() => {
997
1264
  let found = false;
998
1265
  const visit = (n) => {
999
- if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "push")
1266
+ // Vec.push(value, payer) grows the account; FixedVec.push(value) does not.
1267
+ if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "push" && n.arguments.length === 2)
1000
1268
  found = true;
1001
1269
  ts.forEachChild(n, visit);
1002
1270
  };
@@ -1193,6 +1461,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1193
1461
  reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
1194
1462
  if (s.vec)
1195
1463
  reporter.fail(node, `${s.name} ends in a Vec; Zeroed<T> is for fixed-size (large) accounts`);
1464
+ if (s.layout === "borsh")
1465
+ reporter.fail(node, `${s.name} is @layout borsh; Zeroed<T> reads large accounts in place, so it takes fixed layouts only`);
1196
1466
  return { kind: "zeroed", struct: s };
1197
1467
  }
1198
1468
  case "Mut": {
@@ -30,10 +30,17 @@ export declare abstract class EmitterBase {
30
30
  protected readonly helper: Helper | undefined;
31
31
  protected readonly scope: TypeScope;
32
32
  protected readonly checker: ts.TypeChecker;
33
+ protected readonly analysis: Analysis;
34
+ /** Calls already resolved to a helper (a generic one's instance is found by probing its arguments). */
35
+ private readonly calleeCache;
33
36
  /** `file.ts:line` of the statement being lowered. */
34
37
  protected where: string | undefined;
35
38
  protected readonly profile: boolean;
36
39
  constructor(analysis: Analysis, target: Instruction | Helper, opts?: EmitOptions);
40
+ /** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
41
+ protected helperOf(node: ts.CallExpression): Helper | undefined;
42
+ /** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
43
+ private typeArgsOf;
37
44
  protected line(s: string): void;
38
45
  /** `file.ts:line` of a node, for messages. */
39
46
  protected location(node: ts.Node): string;