@typesol/cli 0.16.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.
@@ -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",
@@ -225,7 +227,7 @@ export function analyze(entry) {
225
227
  const isExported = (n) => ts.canHaveModifiers(n) && (ts.getModifiers(n) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
226
228
  const declare = (name, file) => {
227
229
  // 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))
230
+ if (/^(FixedString\d|FixedVec\d|FixedOption_|FixedEnum_)/.test(name.text))
229
231
  reporter.fail(name, `"${name.text}" is reserved for IDL types TypeSOL generates; pick another name`);
230
232
  const prev = declared.get(name.text);
231
233
  if (prev)
@@ -313,7 +315,8 @@ export function analyze(entry) {
313
315
  else {
314
316
  if (stmt.name)
315
317
  declare(stmt.name, file);
316
- 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 });
317
320
  }
318
321
  }
319
322
  else if (ts.isVariableStatement(stmt)) {
@@ -324,6 +327,14 @@ export function analyze(entry) {
324
327
  if (!ts.isIdentifier(d.name) || !d.initializer)
325
328
  reporter.fail(d, "declare const NAME = literal");
326
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
+ }
327
338
  let value;
328
339
  if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && imports.get(init.expression.text) === "pubkey") {
329
340
  const arg = init.arguments[0];
@@ -358,6 +369,7 @@ export function analyze(entry) {
358
369
  }
359
370
  }
360
371
  const fstrings = new Set();
372
+ const fixedVecs = new Map();
361
373
  const scopeFor = (imports) => ({ imports, enums, options, dataEnums });
362
374
  for (const c of pendingConstTypes) {
363
375
  reporter.guard(() => {
@@ -372,14 +384,87 @@ export function analyze(entry) {
372
384
  c.def.type = t;
373
385
  });
374
386
  }
375
- 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));
376
429
  const helpers = new Map();
377
- for (const fn of helperNodes) {
378
- 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));
379
450
  if (h)
380
451
  helpers.set(h.name, h);
381
452
  }
382
- 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));
383
468
  const accountStructs = new Set();
384
469
  const argStructs = new Set();
385
470
  const instructions = [];
@@ -390,6 +475,23 @@ export function analyze(entry) {
390
475
  instructions.push(ix);
391
476
  for (const a of ix.args)
392
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
+ }
393
495
  // A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
394
496
  if (ix.returns?.kind === "struct")
395
497
  collectStructs(ix.returns, structs, dataEnums, argStructs);
@@ -426,6 +528,8 @@ export function analyze(entry) {
426
528
  structs,
427
529
  instructions,
428
530
  helpers,
531
+ generics,
532
+ helperDecls,
429
533
  errors,
430
534
  errorEnums,
431
535
  enums,
@@ -436,12 +540,14 @@ export function analyze(entry) {
436
540
  accountStructs,
437
541
  argStructs,
438
542
  fstrings,
543
+ fixedVecs,
439
544
  },
440
545
  sourceFile,
441
546
  files: programFiles(program, entryPath).map((f) => f.fileName),
442
547
  imports: entryImports,
443
548
  reporter,
444
- checker: program.getTypeChecker(),
549
+ checker,
550
+ instantiate,
445
551
  };
446
552
  }
447
553
  /** Every struct an argument type contains (transitively), for IDL layout decisions. */
@@ -515,6 +621,11 @@ export function typesolName(node, imports) {
515
621
  export function resolveScalarType(node, scope) {
516
622
  if (node.kind === ts.SyntaxKind.BooleanKeyword)
517
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
+ }
518
629
  const n = typesolName(node, scope.imports);
519
630
  if (n && INT_NAMES.includes(n))
520
631
  return { kind: "int", name: n };
@@ -572,7 +683,7 @@ function numericLiteralArg(node) {
572
683
  * Resolve every exported interface. Field types may name other interfaces, so names are
573
684
  * collected first; sizes are computed after, rejecting cycles.
574
685
  */
575
- function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings) {
686
+ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs) {
576
687
  const names = new Set(items.map((d) => d.decl.name.text));
577
688
  const enumNames = new Set(aliases.map((a) => a.decl.name.text));
578
689
  const structs = new Map();
@@ -628,6 +739,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
628
739
  reporter.fail(targs[1], `array length must be 1..${MAX_ARRAY_LEN}`);
629
740
  return { kind: "array", elem, len };
630
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
+ }
631
755
  if (ext === "FixedString") {
632
756
  const cap = numericLiteralArg(targs[0]);
633
757
  if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_STRING_CAP)
@@ -637,7 +761,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
637
761
  }
638
762
  if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && names.has(node.typeName.text) && !targs.length)
639
763
  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");
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");
641
765
  };
642
766
  for (const { decl, imports: fileImports } of items) {
643
767
  scope = scopeFor(fileImports);
@@ -658,12 +782,17 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
658
782
  if (problem)
659
783
  reporter.fail(m.name, problem);
660
784
  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)");
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");
663
787
  fields.push({ name, type });
664
788
  }
665
789
  checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
666
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)");
667
796
  structs.set(name, {
668
797
  name,
669
798
  fields,
@@ -671,6 +800,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
671
800
  accountDiscriminator: discriminator(`account:${name}`),
672
801
  eventDiscriminator: discriminator(`event:${name}`),
673
802
  node: decl,
803
+ layout: layoutText === "borsh" ? "borsh" : "fixed",
674
804
  });
675
805
  });
676
806
  }
@@ -766,8 +896,14 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
766
896
  return undefined;
767
897
  return 1 + e.payload;
768
898
  }
769
- case "vec":
770
- 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
+ }
771
907
  case "string":
772
908
  return undefined;
773
909
  case "fstring":
@@ -806,7 +942,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
806
942
  if (total !== undefined && total + 8 > MAX_ACCOUNT_SIZE)
807
943
  reporter.fail(s.node.name, `${s.name} is ${total + 8} bytes with its discriminator; Solana accounts are at most ${MAX_ACCOUNT_SIZE} bytes`);
808
944
  const last = s.fields.at(-1);
809
- if (last?.type.kind === "vec") {
945
+ if (last?.type.kind === "vec" && last.type.cap === undefined) {
810
946
  const elemSize = sizeOf(last.type.elem, s.node);
811
947
  if (elemSize === undefined)
812
948
  reporter.fail(s.node, "Vec elements must have a fixed size");
@@ -816,24 +952,85 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
816
952
  };
817
953
  for (const s of structs.values())
818
954
  reporter.guard(() => visit(s));
955
+ for (const s of structs.values())
956
+ if (s.layout === "borsh")
957
+ reporter.guard(() => checkBorshLayout(s, structs, reporter));
819
958
  for (const e of dataEnums.values())
820
959
  reporter.guard(() => sizeOf({ kind: "dataEnum", name: e.name }, e.node));
821
960
  return structs;
822
961
  }
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");
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");
828
1025
  if (!fn.type)
829
- reporter.fail(fn.name, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
1026
+ reporter.fail(fn.name ?? fn, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
830
1027
  let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword ? undefined : resolveScalarType(fn.type, scope);
831
1028
  if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns) {
832
1029
  const s = ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments ? structs.get(fn.type.typeName.text) : undefined;
833
1030
  if (s && (s.size === undefined || s.vec))
834
1031
  reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be returned; return a struct of fixed size`);
835
1032
  if (!s)
836
- reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey or a struct");
1033
+ reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, a struct or a type parameter");
837
1034
  returns = { kind: "struct", name: s.name };
838
1035
  }
839
1036
  const params = fn.parameters.map((p) => {
@@ -856,16 +1053,43 @@ function analyzeHelper(fn, reporter, scope, structs) {
856
1053
  reporter.fail(p.type, "helpers take accounts that exist: create Init/InitIfNeeded/Zeroed accounts in the instruction, then pass them as Mut<T>");
857
1054
  return { name: p.name.text, type: { kind: "account" }, account, node: p };
858
1055
  });
859
- 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;
860
1080
  }
861
1081
  /** Helpers may call each other, but not recursively: SBF has a small fixed call depth. */
862
- function checkRecursion(helpers, reporter) {
1082
+ function checkRecursion(sources, decls, checker, reporter) {
863
1083
  const calls = new Map();
864
- for (const h of helpers.values()) {
1084
+ const byName = new Map(sources.map((s) => [s.name, s]));
1085
+ for (const h of sources) {
865
1086
  const out = [];
866
1087
  const visit = (n) => {
867
- if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && helpers.has(n.expression.text))
868
- 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
+ }
869
1093
  ts.forEachChild(n, visit);
870
1094
  };
871
1095
  visit(h.node.body);
@@ -875,14 +1099,15 @@ function checkRecursion(helpers, reporter) {
875
1099
  const walk = (name, path) => {
876
1100
  if (state.get(name) === "done")
877
1101
  return;
1102
+ const src = byName.get(name);
878
1103
  if (state.get(name) === "visiting")
879
- 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(" -> ")}`);
880
1105
  state.set(name, "visiting");
881
1106
  for (const c of calls.get(name) ?? [])
882
1107
  walk(c, [...path, name]);
883
1108
  state.set(name, "done");
884
1109
  };
885
- for (const name of helpers.keys())
1110
+ for (const name of byName.keys())
886
1111
  walk(name, []);
887
1112
  }
888
1113
  function analyzeInstruction(fn, reporter, scope, structs) {
@@ -1038,7 +1263,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
1038
1263
  const pushes = (() => {
1039
1264
  let found = false;
1040
1265
  const visit = (n) => {
1041
- 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)
1042
1268
  found = true;
1043
1269
  ts.forEachChild(n, visit);
1044
1270
  };
@@ -1235,6 +1461,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
1235
1461
  reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
1236
1462
  if (s.vec)
1237
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`);
1238
1466
  return { kind: "zeroed", struct: s };
1239
1467
  }
1240
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;
@@ -1,4 +1,6 @@
1
1
  import ts from "typescript";
2
+ import { calleeName, resolveScalarType } from "../analyze.js";
3
+ import { isScalar, } from "../model.js";
2
4
  import { isStable, cString, valueC } from "./shared.js";
3
5
  /**
4
6
  * State of one function being lowered, and the primitives every layer uses: emitting lines, temporaries,
@@ -27,6 +29,9 @@ export class EmitterBase {
27
29
  helper;
28
30
  scope;
29
31
  checker;
32
+ analysis;
33
+ /** Calls already resolved to a helper (a generic one's instance is found by probing its arguments). */
34
+ calleeCache = new Map();
30
35
  /** `file.ts:line` of the statement being lowered. */
31
36
  where;
32
37
  profile;
@@ -36,8 +41,15 @@ export class EmitterBase {
36
41
  this.model = analysis.model;
37
42
  // Each function resolves `typesol` names through its own file's imports.
38
43
  this.imports = target.imports;
39
- this.scope = { imports: target.imports, enums: analysis.model.enums, options: analysis.model.options, dataEnums: analysis.model.dataEnums };
44
+ this.scope = {
45
+ imports: target.imports,
46
+ enums: analysis.model.enums,
47
+ options: analysis.model.options,
48
+ dataEnums: analysis.model.dataEnums,
49
+ typeArgs: "accounts" in target ? undefined : target.typeArgs,
50
+ };
40
51
  this.checker = analysis.checker;
52
+ this.analysis = analysis;
41
53
  if ("accounts" in target) {
42
54
  this.ix = target;
43
55
  for (const a of target.accounts) {
@@ -66,6 +78,54 @@ export class EmitterBase {
66
78
  }
67
79
  }
68
80
  }
81
+ /** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
82
+ helperOf(node) {
83
+ if (!ts.isIdentifier(node.expression))
84
+ return undefined;
85
+ const cached = this.calleeCache.get(node);
86
+ if (cached !== undefined)
87
+ return cached ?? undefined;
88
+ const callee = node.expression;
89
+ const name = calleeName(callee, this.model.helperDecls, this.checker) ?? (this.lookup(callee.text) ? undefined : callee.text);
90
+ const g = name ? this.model.generics.get(name) : undefined;
91
+ let h = name ? this.model.helpers.get(name) : undefined;
92
+ if (g)
93
+ h = this.analysis.instantiate(g, this.typeArgsOf(node, g));
94
+ else if (h && node.typeArguments)
95
+ this.fail(node, `${h.display} takes no type arguments`);
96
+ this.calleeCache.set(node, h ?? null);
97
+ return h;
98
+ }
99
+ /** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
100
+ typeArgsOf(node, g) {
101
+ const usage = `write ${g.display}<u64>(…) with the types`;
102
+ if (node.typeArguments) {
103
+ if (node.typeArguments.length !== g.typeParams.length)
104
+ this.fail(node, `${g.display} takes ${g.typeParams.length} type argument(s)`);
105
+ return node.typeArguments.map((t) => resolveScalarType(t, this.scope) ?? this.fail(t, "type arguments are integers, boolean, Pubkey or enums"));
106
+ }
107
+ return g.typeParams.map((tp) => {
108
+ for (const [i, p] of g.node.parameters.entries()) {
109
+ const arg = node.arguments[i];
110
+ if (!arg || !p.type || !ts.isTypeReferenceNode(p.type) || !ts.isIdentifier(p.type.typeName) || p.type.typeName.text !== tp || p.type.typeArguments)
111
+ continue;
112
+ // Type the argument without emitting it: it is lowered again, once, by the call.
113
+ const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
114
+ const probe = this.reporter.guard(() => this.expr(arg));
115
+ this.lines.length = saved.lines;
116
+ this.hoisted.length = saved.hoisted;
117
+ this.temp = saved.temp;
118
+ this.reporter.diagnostics.length = saved.diagnostics;
119
+ if (!probe || probe.literal !== undefined || probe.isNull)
120
+ continue;
121
+ if (!isScalar(probe.type))
122
+ this.fail(arg, `${tp} stands for an integer, boolean, Pubkey or enum, not ${probe.type.kind}`);
123
+ const t = probe.type;
124
+ return t.kind === "pubkey" ? (t.hash ? { kind: "pubkey", hash: true } : { kind: "pubkey" }) : t;
125
+ }
126
+ return this.fail(node, `cannot tell what ${tp} is from literal arguments; ${usage}`);
127
+ });
128
+ }
69
129
  line(s) {
70
130
  // Runtime failures name the TypeScript line they come from: "TypeSOL: arithmetic overflow (vault.ts:42)".
71
131
  if (this.where)
@@ -0,0 +1,13 @@
1
+ import type { FieldType, ProgramModel, StructDef } from "../model.js";
2
+ /**
3
+ * Accounts declared `@layout borsh` are stored as Anchor stores them (Borsh: a String or Vec takes 4 + its length, an
4
+ * empty Option one byte, and the fields after them move). An instruction unpacks such an account into the usual
5
+ * fixed-layout struct on the heap (`ts_unpack_<S>`, checking it as Borsh deserialization would), runs on that, and
6
+ * packs it back when it returns (`ts_pack_<S>`), so every language feature works on it unchanged.
7
+ */
8
+ /** A field type whose Borsh bytes are its fixed-layout bytes: scalars, Pubkeys, unit enums, arrays and structs of them. */
9
+ export declare function borshIdentical(t: FieldType, model: Pick<ProgramModel, "structs">): boolean;
10
+ /** The fewest bytes a Borsh `S` can take (every String and Vec empty, every Option None). */
11
+ export declare function borshMinSize(s: StructDef, model: Pick<ProgramModel, "structs">): number;
12
+ /** C for `ts_unpack_<S>` and `ts_pack_<S>` of every `@layout borsh` struct. */
13
+ export declare function emitBorshCodecs(model: ProgramModel): string[];