@typesol/cli 0.22.0 → 0.24.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 (40) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +13 -12
  3. package/SECURITY.md +1 -1
  4. package/dist/clientgen.js +190 -41
  5. package/dist/compiler/analyze.js +195 -25
  6. package/dist/compiler/emit/base.d.ts +28 -5
  7. package/dist/compiler/emit/base.js +81 -15
  8. package/dist/compiler/emit/calls.d.ts +18 -1
  9. package/dist/compiler/emit/calls.js +159 -7
  10. package/dist/compiler/emit/cpi.d.ts +23 -0
  11. package/dist/compiler/emit/cpi.js +274 -5
  12. package/dist/compiler/emit/expressions.d.ts +9 -1
  13. package/dist/compiler/emit/expressions.js +25 -12
  14. package/dist/compiler/emit/function.js +8 -4
  15. package/dist/compiler/emit/program.js +9 -2
  16. package/dist/compiler/emit/shared.d.ts +43 -3
  17. package/dist/compiler/emit/shared.js +43 -0
  18. package/dist/compiler/emit/statements.d.ts +39 -2
  19. package/dist/compiler/emit/statements.js +346 -87
  20. package/dist/compiler/emit/storage.d.ts +6 -2
  21. package/dist/compiler/emit/storage.js +75 -45
  22. package/dist/compiler/model.d.ts +26 -1
  23. package/dist/idlpublish.d.ts +6 -1
  24. package/dist/idlpublish.js +47 -5
  25. package/dist/portgen.js +1 -1
  26. package/dist/svmrpc.d.ts +7 -1
  27. package/dist/svmrpc.js +133 -19
  28. package/dist/testing.js +23 -3
  29. package/docs/cli.md +2 -2
  30. package/docs/client.md +13 -10
  31. package/docs/errors.md +9 -3
  32. package/docs/language.md +21 -20
  33. package/docs/mainnet.md +1 -1
  34. package/docs/migrate-from-anchor.md +8 -9
  35. package/docs/test-your-program.md +6 -2
  36. package/docs/testing.md +1 -1
  37. package/docs/troubleshooting.md +3 -2
  38. package/lib/typesol.d.ts +127 -20
  39. package/package.json +1 -1
  40. package/runtime/typesol_rt.h +73 -18
@@ -422,10 +422,84 @@ export function analyze(entry) {
422
422
  });
423
423
  return;
424
424
  }
425
+ // An arrow written as a helper's function argument (`apply((x: u64): u64 => x + 1n, v)`): a local function of
426
+ // its own, passed like a named one. Missing parameter and return types come from the helper's parameter type.
427
+ const found = (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && ts.isCallExpression(n.parent) ? fnParamType(n, checker) : undefined;
428
+ if (found) {
429
+ reporter.guard(() => {
430
+ const { type: target, owner } = found;
431
+ if (generic)
432
+ reporter.fail(n, "arrow functions passed inside a generic helper are not supported; declare the function at the top level");
433
+ const lit = n;
434
+ // Types left out are taken from the helper's parameter type, as written in the helper's file: its names are
435
+ // resolved with that file's imports (added to this function's), and its type parameters cannot be.
436
+ const typeParams = new Set((owner.typeParameters ?? []).map((t) => t.name.text));
437
+ const usesTypeParam = (t) => (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && typeParams.has(t.typeName.text)) ||
438
+ !!ts.forEachChild(t, (c) => usesTypeParam(c) || undefined);
439
+ const ownerImports = importsOf.get(owner.getSourceFile()) ?? imports;
440
+ let lifted = imports;
441
+ const graft = (from, at) => {
442
+ if (!from)
443
+ return undefined;
444
+ // `T` itself, given at the call (`applyT<u64>((v) => …)`): the type written there.
445
+ const call = n.parent;
446
+ if (ts.isTypeReferenceNode(from) && ts.isIdentifier(from.typeName) && typeParams.has(from.typeName.text) && call.typeArguments) {
447
+ const i = (owner.typeParameters ?? []).findIndex((t) => t.name.text === from.typeName.text);
448
+ const given = call.typeArguments[i];
449
+ if (given)
450
+ return given;
451
+ }
452
+ if (usesTypeParam(from))
453
+ reporter.fail(at, `write the types here: the parameter's type uses the helper's type parameter (${[...typeParams].join(", ")})`);
454
+ if (ownerImports !== imports) {
455
+ const names = new Set();
456
+ const collect = (t) => {
457
+ if (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName))
458
+ names.add(t.typeName.text);
459
+ ts.forEachChild(t, collect);
460
+ };
461
+ collect(from);
462
+ for (const nm of names) {
463
+ const theirs = ownerImports.get(nm);
464
+ if (theirs === undefined)
465
+ continue;
466
+ const ours = lifted.get(nm);
467
+ if (ours !== undefined && ours !== theirs)
468
+ reporter.fail(at, `write the types here: "${nm}" means ${theirs} in the helper's file and ${ours} here`);
469
+ if (ours === undefined) {
470
+ if (lifted === imports)
471
+ lifted = new Map(imports);
472
+ lifted.set(nm, theirs);
473
+ }
474
+ }
475
+ }
476
+ return from;
477
+ };
478
+ lit.parameters.forEach((p, i) => {
479
+ const from = graft(p.type ? undefined : target.parameters[i]?.type, p);
480
+ if (from)
481
+ p.type = from;
482
+ });
483
+ if (!lit.type)
484
+ lit.type = graft(target.type, lit);
485
+ let name = `${outer}__fn`;
486
+ for (let i = 2; taken.has(name); i++)
487
+ name = `${outer}__fn${i}`;
488
+ taken.add(name);
489
+ const src = { node: lit, name, display: "the function passed here", decl: lit, imports: lifted, outer: fn };
490
+ locals.push(src);
491
+ if (lit.body)
492
+ collectLocals(lit.body, name, imports, lit);
493
+ });
494
+ return;
495
+ }
425
496
  ts.forEachChild(n, visit);
426
497
  };
427
498
  visit(body);
428
499
  };
500
+ const importsOf = new Map();
501
+ for (const f of [...functions, ...helperNodes])
502
+ importsOf.set(f.node.getSourceFile(), f.imports);
429
503
  for (const f of functions)
430
504
  if (f.node.body && f.node.name)
431
505
  collectLocals(f.node.body, f.node.name.text, f.imports, f.node);
@@ -448,8 +522,13 @@ export function analyze(entry) {
448
522
  const helpers = new Map();
449
523
  const generics = new Map();
450
524
  const helperDecls = new Map();
525
+ const factories = new Map();
451
526
  for (const fn of [...helperNodes, ...locals.filter((l) => !inline.has(l.decl))]) {
452
527
  helperDecls.set(fn.decl, fn.name);
528
+ if (returnsFunction(fn.node)) {
529
+ factories.set(fn.name, { node: fn.node, imports: fn.imports });
530
+ continue;
531
+ }
453
532
  const fnParams = fn.node.parameters.map((p, i) => (p.type && ts.isFunctionTypeNode(p.type) ? i : -1)).filter((i) => i >= 0);
454
533
  if (fn.node.typeParameters?.length || fnParams.length) {
455
534
  reporter.guard(() => {
@@ -519,14 +598,6 @@ export function analyze(entry) {
519
598
  if (structs.get(s).layout === "borsh" && s !== whole)
520
599
  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`);
521
600
  }
522
- // Arguments are Borsh: a FixedVec's padded layout is account data only (a @layout borsh struct's FixedVec is a Borsh Vec).
523
- for (const a of ix.args) {
524
- const seen = new Set();
525
- collectStructs(a.type, structs, dataEnums, seen);
526
- for (const s of seen)
527
- if (structs.get(s).layout !== "borsh" && structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
528
- 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`);
529
- }
530
601
  // A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
531
602
  if (ix.returns?.kind === "struct")
532
603
  collectStructs(ix.returns, structs, dataEnums, argStructs);
@@ -537,6 +608,39 @@ export function analyze(entry) {
537
608
  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>`);
538
609
  }
539
610
  }
611
+ // Arguments are Borsh, where a FixedVec is a Vec (only what it holds). A struct passed as a whole argument and used
612
+ // nowhere else (not account data, not nested, not returned) is read as Anchor sends it: @layout borsh, implied.
613
+ // Anywhere else its FixedVec keeps the padded layout of account data, which an argument cannot have.
614
+ const hasFixedVec = (n) => structs.get(n).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined);
615
+ const holds = (f, n) => {
616
+ const inner = f.type.kind === "array" || f.type.kind === "vec" ? f.type.elem : f.type.kind === "option" ? f.type.inner : f.type;
617
+ return inner.kind === "struct" && inner.name === n;
618
+ };
619
+ // In another struct, or in a data enum's variant (stored in its fixed layout in account data).
620
+ const nestedSomewhere = (n) => [...structs.values()].some((o) => o.fields.some((f) => holds(f, n))) ||
621
+ [...dataEnums.values()].some((e) => e.variants.some((v) => v.fields.some((f) => holds(f, n))));
622
+ for (const ix of instructions)
623
+ for (const a of ix.args) {
624
+ const seen = new Set();
625
+ collectStructs(a.type, structs, dataEnums, seen);
626
+ const whole = a.type.kind === "structRef" ? a.type.struct : undefined;
627
+ for (const n of seen) {
628
+ const sd = structs.get(n);
629
+ if (sd.layout === "borsh" || !hasFixedVec(n))
630
+ continue;
631
+ const onlyArgument = n === whole &&
632
+ !accountStructs.has(n) &&
633
+ !nestedSomewhere(n) &&
634
+ !instructions.some((i) => i.returns?.kind === "struct" && i.returns.name === n) &&
635
+ !instructions.some((i) => i.args.some((x) => x !== a && x.type.kind !== "structRef" && JSON.stringify(x.type).includes(`"${n}"`)));
636
+ if (onlyArgument) {
637
+ sd.layout = "borsh";
638
+ reporter.guard(() => checkBorshLayout(sd, structs, dataEnums, reporter));
639
+ continue;
640
+ }
641
+ reporter.error(ix.node.name ?? ix.node, `argument ${a.name}: ${n} holds a FixedVec and is also account data, nested or returned (a fixed layout there, Borsh in arguments); declare a separate struct for the argument, or pass the list as its own FixedVec<T, N> argument`);
642
+ }
643
+ }
540
644
  checkSnakeDuplicates(instructions.map((ix) => ({ name: ix.node.name.text, node: ix.node.name })), "instructions", reporter);
541
645
  // emitCpi's event instruction (Anchor's EVENT_IX_TAG) is dispatched before every other instruction.
542
646
  if (instructions.some((ix) => ix.accounts.some((a) => a.kind.kind === "eventAuthority"))) {
@@ -569,6 +673,7 @@ export function analyze(entry) {
569
673
  generics,
570
674
  helperDecls,
571
675
  closureDecls,
676
+ factories,
572
677
  errors,
573
678
  errorEnums,
574
679
  enums,
@@ -580,6 +685,7 @@ export function analyze(entry) {
580
685
  argStructs,
581
686
  fstrings,
582
687
  fixedVecs,
688
+ localVecs: new Map(),
583
689
  },
584
690
  sourceFile,
585
691
  files: programFiles(program, entryPath).map((f) => f.fileName),
@@ -642,11 +748,15 @@ export function resolveArgAggregate(node, scope, structs) {
642
748
  return { kind: "structRef", struct: top.name, writable: false };
643
749
  if (top?.kind === "array")
644
750
  return { kind: "arrayArg", array: top };
645
- if (typesolName(node, scope.imports) === "Vec") {
646
- const e = node.typeArguments?.[0];
751
+ const list = typesolName(node, scope.imports);
752
+ if (list === "Vec" || list === "FixedVec") {
753
+ const [e, n] = node.typeArguments ?? [];
647
754
  const elem = e && fixed(e);
755
+ const cap = list === "FixedVec" ? numericLiteralArg(n) : undefined;
756
+ if (list === "FixedVec" && !(cap !== undefined && Number.isInteger(cap) && cap >= 1))
757
+ return undefined;
648
758
  if (elem && (elem.kind === "struct" || elem.kind === "array"))
649
- return { kind: "vecArg", elem };
759
+ return { kind: "vecArg", elem, ...(cap ? { cap } : {}) };
650
760
  }
651
761
  return undefined;
652
762
  }
@@ -714,6 +824,14 @@ export function resolveValueType(node, scope, allowVec = false) {
714
824
  if (elem)
715
825
  return { kind: "vecArg", elem };
716
826
  }
827
+ // FixedVec<T, N> as an argument: a Borsh Vec of at most N elements (more fail to deserialize), read like a Vec argument.
828
+ if (allowVec && typesolName(node, scope.imports) === "FixedVec") {
829
+ const [elemNode, n] = node.typeArguments ?? [];
830
+ const elem = elemNode && resolveScalarType(elemNode, scope);
831
+ const cap = numericLiteralArg(n);
832
+ if (elem && cap !== undefined && Number.isInteger(cap) && cap >= 1)
833
+ return { kind: "vecArg", elem, cap };
834
+ }
717
835
  return resolveScalarType(node, scope);
718
836
  }
719
837
  function numericLiteralArg(node) {
@@ -803,7 +921,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
803
921
  }
804
922
  if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && names.has(node.typeName.text) && !targs.length)
805
923
  return { kind: "struct", name: node.typeName.text };
806
- 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");
924
+ reporter.fail(node, "field type must be an integer, boolean, Pubkey, FixedArray<T, N>, FixedString<N>, FixedVec<T, N>, Vec<T>, string (events only) or another struct");
807
925
  };
808
926
  for (const { decl, imports: fileImports } of items) {
809
927
  scope = scopeFor(fileImports);
@@ -824,9 +942,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
824
942
  if (problem)
825
943
  reporter.fail(m.name, problem);
826
944
  const type = fieldType(m.type);
827
- if (type.kind === "vec" && type.cap === undefined && m !== decl.members[decl.members.length - 1])
828
- 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");
829
- fields.push({ name, type });
945
+ fields.push({ name, type, node: m });
830
946
  }
831
947
  checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
832
948
  const name = decl.name.text;
@@ -835,9 +951,20 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
835
951
  const layoutText = typeof layoutTag?.comment === "string" ? layoutTag.comment.trim() : undefined;
836
952
  if (layoutTag && layoutText !== "borsh" && layoutText !== "fixed")
837
953
  reporter.fail(layoutTag, "@layout is borsh (Anchor's account bytes) or fixed (the default)");
954
+ const growable = (f) => f.type.kind === "vec" && f.type.cap === undefined;
955
+ // Anchor's bytes keep the declared order, so there a growable Vec has to be last (the fields after it would move).
956
+ if (layoutText === "borsh")
957
+ for (const f of fields.slice(0, -1))
958
+ if (growable(f))
959
+ reporter.fail(f.node ?? decl, "in a @layout borsh struct a Vec<T> field must be the last field (Anchor's bytes keep the declared order); use FixedVec<T, N> for a list of at most N elsewhere");
960
+ // The fixed layout stores the growable Vecs after the fixed-size fields, in the order declared, so every other
961
+ // field keeps a fixed offset; the IDL lists the fields in this stored order.
962
+ const stored = layoutText === "borsh" ? fields : [...fields.filter((f) => !growable(f)), ...fields.filter(growable)];
963
+ for (const f of stored)
964
+ delete f.node;
838
965
  structs.set(name, {
839
966
  name,
840
- fields,
967
+ fields: stored,
841
968
  size: undefined,
842
969
  accountDiscriminator: discriminator(`account:${name}`),
843
970
  eventDiscriminator: discriminator(`event:${name}`),
@@ -964,7 +1091,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
964
1091
  if (s.size === undefined)
965
1092
  reporter.fail(at, `${t.name} has a string field and cannot be nested; use FixedString<N>`);
966
1093
  if (s.vec)
967
- reporter.fail(at, `${t.name} ends in a Vec and can only be account data, not nested`);
1094
+ reporter.fail(at, `${t.name} has a growable Vec and can only be account data, not nested`);
968
1095
  return s.size;
969
1096
  }
970
1097
  }
@@ -987,13 +1114,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
987
1114
  s.size = total;
988
1115
  if (total !== undefined && total + 8 > MAX_ACCOUNT_SIZE)
989
1116
  reporter.fail(s.node.name, `${s.name} is ${total + 8} bytes with its discriminator; Solana accounts are at most ${MAX_ACCOUNT_SIZE} bytes`);
990
- const last = s.fields.at(-1);
991
- if (last?.type.kind === "vec" && last.type.cap === undefined) {
992
- const elemSize = sizeOf(last.type.elem, s.node);
993
- if (elemSize === undefined)
994
- reporter.fail(s.node, "Vec elements must have a fixed size");
995
- s.vec = { name: last.name, elem: last.type.elem, elemSize };
996
- }
1117
+ // Growable Vecs (stored last, see above): the first one's length and elements end the C struct; the others follow
1118
+ // it in the account, each a u32 length and its elements.
1119
+ for (const f of s.fields)
1120
+ if (f.type.kind === "vec" && f.type.cap === undefined) {
1121
+ const elemSize = sizeOf(f.type.elem, s.node);
1122
+ if (elemSize === undefined)
1123
+ reporter.fail(s.node, "Vec elements must have a fixed size");
1124
+ (s.vecs ??= []).push({ name: f.name, elem: f.type.elem, elemSize });
1125
+ }
1126
+ s.vec = s.vecs?.[0];
1127
+ // The C struct holds the first Vec's length; each further one adds its own when the account is created.
1128
+ if (s.vecs && s.size !== undefined)
1129
+ s.size -= 4 * (s.vecs.length - 1);
997
1130
  state.set(s.name, "done");
998
1131
  };
999
1132
  for (const s of structs.values())
@@ -1007,6 +1140,43 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
1007
1140
  }
1008
1141
  const isFunctionNode = (n) => ts.isFunctionDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isMethodDeclaration(n);
1009
1142
  /** Whether a local function reads a variable or parameter of a function around it (a closure). */
1143
+ /**
1144
+ * The function type of the helper parameter an arrow is passed for (`f` in `function apply(f: (x: u64) => u64, …)`),
1145
+ * when the arrow is an argument of a call to a declared function or const arrow; undefined otherwise.
1146
+ */
1147
+ function fnParamType(arrow, checker) {
1148
+ const call = arrow.parent;
1149
+ const i = call.arguments.indexOf(arrow);
1150
+ if (i < 0 || !ts.isIdentifier(call.expression))
1151
+ return undefined;
1152
+ let sym = checker.getSymbolAtLocation(call.expression);
1153
+ if (sym && sym.flags & ts.SymbolFlags.Alias)
1154
+ sym = checker.getAliasedSymbol(sym);
1155
+ for (const d of sym?.declarations ?? []) {
1156
+ const fn = ts.isFunctionDeclaration(d)
1157
+ ? d
1158
+ : ts.isVariableDeclaration(d) && d.initializer && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))
1159
+ ? d.initializer
1160
+ : undefined;
1161
+ const t = fn?.parameters[i]?.type;
1162
+ if (fn && t && ts.isFunctionTypeNode(t))
1163
+ return { type: t, owner: fn };
1164
+ }
1165
+ return undefined;
1166
+ }
1167
+ /** Whether a function returns a function: typed so, or its body is (or only returns) an arrow. */
1168
+ function returnsFunction(fn) {
1169
+ if (fn.type)
1170
+ return ts.isFunctionTypeNode(fn.type);
1171
+ const strip = (e) => (e && ts.isParenthesizedExpression(e) ? strip(e.expression) : e);
1172
+ const body = fn.body;
1173
+ const ret = body && !ts.isBlock(body)
1174
+ ? strip(body)
1175
+ : body?.statements.length === 1 && ts.isReturnStatement(body.statements[0])
1176
+ ? strip(body.statements[0].expression)
1177
+ : undefined;
1178
+ return !!ret && (ts.isArrowFunction(ret) || ts.isFunctionExpression(ret));
1179
+ }
1010
1180
  function captures(l, localDecls, checker) {
1011
1181
  const fn = l.node;
1012
1182
  let found = false;
@@ -1574,7 +1744,7 @@ function resolveAccountKind(node, reporter, imports, structs) {
1574
1744
  if (!s)
1575
1745
  reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
1576
1746
  if (s.vec)
1577
- reporter.fail(node, `${s.name} ends in a Vec; Zeroed<T> is for fixed-size (large) accounts`);
1747
+ reporter.fail(node, `${s.name} has a growable Vec; Zeroed<T> is for fixed-size (large) accounts`);
1578
1748
  if (s.layout === "borsh")
1579
1749
  reporter.fail(node, `${s.name} is @layout borsh; Zeroed<T> reads large accounts in place, so it takes fixed layouts only`);
1580
1750
  return { kind: "zeroed", struct: s };
@@ -1,7 +1,7 @@
1
1
  import ts from "typescript";
2
- import { type Analysis, type TypeScope } from "../analyze.js";
2
+ import { type Analysis, type TypeArg, type TypeScope } from "../analyze.js";
3
3
  import type { Reporter } from "../diagnostics.js";
4
- import { type AccountParam, type Helper, type IdlSeed, type Instruction, type ProgramModel, type ValueType } from "../model.js";
4
+ import { type AccountParam, type GenericHelper, type Helper, type IdlSeed, type Instruction, type ProgramModel, type ValueType } from "../model.js";
5
5
  import { type Value, type Ref, type Local, type Closure, type EmitOptions } from "./shared.js";
6
6
  /**
7
7
  * State of one function being lowered, and the primitives every layer uses: emitting lines, temporaries,
@@ -32,6 +32,8 @@ export declare abstract class EmitterBase {
32
32
  declAt: number;
33
33
  pad: string;
34
34
  node: ts.Node;
35
+ /** A place the result is written to (a stored `map` element), instead of a value. */
36
+ into?: Ref;
35
37
  }[];
36
38
  /** Closures being expanded (a closure calling itself cannot be expanded). */
37
39
  protected readonly inlining: Set<ts.Node>;
@@ -39,7 +41,8 @@ export declare abstract class EmitterBase {
39
41
  protected readonly loops: string[];
40
42
  protected readonly ix: Instruction | undefined;
41
43
  protected readonly helper: Helper | undefined;
42
- protected readonly scope: TypeScope;
44
+ /** Type names in effect (a generic helper expanded where it is called binds its type parameters here). */
45
+ protected scope: TypeScope;
43
46
  protected readonly checker: ts.TypeChecker;
44
47
  protected readonly analysis: Analysis;
45
48
  /** Calls already resolved to a helper (a generic one's instance is found by probing its arguments). */
@@ -50,6 +53,8 @@ export declare abstract class EmitterBase {
50
53
  constructor(analysis: Analysis, target: Instruction | Helper, opts?: EmitOptions);
51
54
  /** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
52
55
  protected helperOf(node: ts.CallExpression): Helper | undefined;
56
+ /** The helper a call runs, when it is given a closure for a function parameter (so it is expanded there). */
57
+ protected givenClosure(node: ts.CallExpression): GenericHelper | undefined;
53
58
  /**
54
59
  * The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
55
60
  * captures nothing). A closure can only run where it is declared, so it cannot be passed.
@@ -59,7 +64,7 @@ export declare abstract class EmitterBase {
59
64
  private structArg;
60
65
  private fixedStruct;
61
66
  /** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
62
- private typeArgsOf;
67
+ protected typeArgsOf(node: ts.CallExpression, g: GenericHelper): TypeArg[];
63
68
  protected line(s: string): void;
64
69
  /** `file.ts:line` of a node, for messages. */
65
70
  protected location(node: ts.Node): string;
@@ -76,11 +81,16 @@ export declare abstract class EmitterBase {
76
81
  protected noOptionalChain(node: ts.Node): void;
77
82
  protected lookup(name: string): Local | undefined;
78
83
  protected block(fn: () => void): void;
84
+ /**
85
+ * Run `fn` to learn something (a type, a storage kind) without keeping anything it did: the emitted and hoisted
86
+ * lines, temporaries, diagnostics and emitter state are put back. Undefined when `fn` fails.
87
+ */
88
+ protected probe<T>(fn: () => T): T | undefined;
79
89
  protected fail(node: ts.Node, message: string): never;
80
90
  protected exportName(node: ts.Expression): string | undefined;
81
91
  /**
82
92
  * `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
83
- * failing with 3020 otherwise). Returns undefined for anything else.
93
+ * failing with 5115 otherwise). Returns undefined for anything else.
84
94
  */
85
95
  protected withoutBang(node: ts.Expression): ts.PropertyAccessExpression | undefined;
86
96
  protected accountParam(node: ts.Expression, what: string): AccountParam;
@@ -116,4 +126,17 @@ export declare abstract class EmitterBase {
116
126
  protected abstract epilogue(): string[];
117
127
  protected abstract expandClosure(c: Closure, args: readonly ts.Expression[], node: ts.Node): Value | undefined;
118
128
  protected abstract listMethod(node: ts.CallExpression, method: string): Value | undefined;
129
+ protected abstract isPipeline(e: ts.Expression): e is ts.CallExpression;
130
+ protected abstract closureOf(arg: ts.Expression): Closure;
131
+ /** The arguments and accounts code written here can name (what a closure written here keeps). */
132
+ protected envNow(): Closure["env"];
133
+ /** None: a helper's or factory's body sees its own parameters and the module only. */
134
+ protected noEnv(): Closure["env"];
135
+ protected abstract madeFunction(call: ts.CallExpression): Closure | undefined;
136
+ protected abstract eachElement(list: ts.Expression, at: ts.Node, names: {
137
+ index: string;
138
+ ci: string;
139
+ next?: string;
140
+ alwaysNext?: boolean;
141
+ }, body: (element: ts.Expression, index: ts.Expression) => void): void;
119
142
  }
@@ -31,6 +31,7 @@ export class EmitterBase {
31
31
  loops = [];
32
32
  ix;
33
33
  helper;
34
+ /** Type names in effect (a generic helper expanded where it is called binds its type parameters here). */
34
35
  scope;
35
36
  checker;
36
37
  analysis;
@@ -87,6 +88,9 @@ export class EmitterBase {
87
88
  helperOf(node) {
88
89
  if (!ts.isIdentifier(node.expression))
89
90
  return undefined;
91
+ // Given a closure, the helper is expanded where it is called (helperWithClosure), not called.
92
+ if (this.givenClosure(node))
93
+ return undefined;
90
94
  const cached = this.calleeCache.get(node);
91
95
  if (cached !== undefined)
92
96
  return cached ?? undefined;
@@ -103,6 +107,17 @@ export class EmitterBase {
103
107
  this.calleeCache.set(node, h ?? null);
104
108
  return h;
105
109
  }
110
+ /** The helper a call runs, when it is given a closure for a function parameter (so it is expanded there). */
111
+ givenClosure(node) {
112
+ if (!ts.isIdentifier(node.expression))
113
+ return undefined;
114
+ const name = calleeName(node.expression, this.model.helperDecls, this.checker) ?? (this.lookup(node.expression.text) ? undefined : node.expression.text);
115
+ const g = name ? this.model.generics.get(name) : undefined;
116
+ if (!g?.fnParams.length)
117
+ return undefined;
118
+ const closureArg = (a) => !!a && (ts.isArrowFunction(a) || ts.isFunctionExpression(a) ? !this.model.helperDecls.has(a) : ts.isIdentifier(a) && !!this.lookup(a.text)?.closure);
119
+ return g.fnParams.some((i) => closureArg(node.arguments[i])) ? g : undefined;
120
+ }
106
121
  /**
107
122
  * The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
108
123
  * captures nothing). A closure can only run where it is declared, so it cannot be passed.
@@ -111,6 +126,13 @@ export class EmitterBase {
111
126
  return g.fnParams.map((i) => {
112
127
  const arg = node.arguments[i];
113
128
  const pname = g.node.parameters[i].name.text;
129
+ // An arrow written here: lifted to a local function when it reads nothing around it.
130
+ if (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))) {
131
+ const lifted = this.model.helperDecls.get(arg);
132
+ if (lifted)
133
+ return lifted;
134
+ this.fail(arg, `this function reads the variables of the function around it, so it can only run there, not be passed to ${g.display}; pass the values it needs as arguments`);
135
+ }
114
136
  if (!arg || !ts.isIdentifier(arg))
115
137
  this.fail(arg ?? node, `pass ${g.display}'s ${pname} a named function (declare it: const f = (…) => …)`);
116
138
  const passed = this.helper?.fnArgs?.get(arg.text);
@@ -151,21 +173,12 @@ export class EmitterBase {
151
173
  if (!arg || !p.type || !ts.isTypeReferenceNode(p.type) || !ts.isIdentifier(p.type.typeName) || p.type.typeName.text !== tp || p.type.typeArguments)
152
174
  continue;
153
175
  // Type the argument without emitting it: it is lowered again, once, by the call.
154
- const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
155
- const probe = this.reporter.guard(() => this.expr(arg));
156
- this.lines.length = saved.lines;
157
- this.hoisted.length = saved.hoisted;
158
- this.temp = saved.temp;
159
- this.reporter.diagnostics.length = saved.diagnostics;
176
+ const probe = this.probe(() => this.expr(arg));
160
177
  if (probe?.type.kind === "structRef")
161
178
  return this.fixedStruct(probe.type.struct, arg);
162
179
  if (!probe) {
163
180
  // A struct place (`p.a`) is not a value: type it as a reference, again without emitting anything.
164
- const r = this.reporter.guard(() => this.ref(arg));
165
- this.lines.length = saved.lines;
166
- this.hoisted.length = saved.hoisted;
167
- this.temp = saved.temp;
168
- this.reporter.diagnostics.length = saved.diagnostics;
181
+ const r = this.probe(() => this.ref(arg));
169
182
  if (r?.type.kind === "struct")
170
183
  return this.fixedStruct(r.type.name, arg);
171
184
  }
@@ -262,9 +275,54 @@ export class EmitterBase {
262
275
  block(fn) {
263
276
  this.scopes.push(new Map());
264
277
  this.indent++;
265
- fn();
266
- this.indent--;
267
- this.scopes.pop();
278
+ try {
279
+ fn();
280
+ }
281
+ finally {
282
+ this.indent--;
283
+ this.scopes.pop();
284
+ }
285
+ }
286
+ /**
287
+ * Run `fn` to learn something (a type, a storage kind) without keeping anything it did: the emitted and hoisted
288
+ * lines, temporaries, diagnostics and emitter state are put back. Undefined when `fn` fails.
289
+ */
290
+ probe(fn) {
291
+ const saved = {
292
+ lines: this.lines.length,
293
+ hoisted: this.hoisted.length, // a probed helper call may hoist declarations
294
+ temp: this.temp,
295
+ diagnostics: this.reporter.diagnostics.length,
296
+ scopes: this.scopes.length,
297
+ indent: this.indent,
298
+ loops: this.loops.length,
299
+ frames: this.inlineFrames.length,
300
+ created: new Set(this.created),
301
+ pendingInit: new Set(this.pendingInit),
302
+ bumps: new Map(this.bumps),
303
+ };
304
+ try {
305
+ return this.reporter.guard(fn);
306
+ }
307
+ finally {
308
+ this.lines.length = saved.lines;
309
+ this.hoisted.length = saved.hoisted;
310
+ this.temp = saved.temp;
311
+ this.reporter.diagnostics.length = saved.diagnostics;
312
+ this.scopes.length = saved.scopes;
313
+ this.indent = saved.indent;
314
+ this.loops.length = saved.loops;
315
+ this.inlineFrames.length = saved.frames;
316
+ for (const [set, from] of [
317
+ [this.created, saved.created],
318
+ [this.pendingInit, saved.pendingInit],
319
+ ]) {
320
+ set.clear();
321
+ from.forEach((x) => set.add(x));
322
+ }
323
+ this.bumps.clear();
324
+ saved.bumps.forEach((v, k) => this.bumps.set(k, v));
325
+ }
268
326
  }
269
327
  fail(node, message) {
270
328
  return this.reporter.fail(node, message);
@@ -274,7 +332,7 @@ export class EmitterBase {
274
332
  }
275
333
  /**
276
334
  * `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
277
- * failing with 3020 otherwise). Returns undefined for anything else.
335
+ * failing with 5115 otherwise). Returns undefined for anything else.
278
336
  */
279
337
  withoutBang(node) {
280
338
  if (!ts.isPropertyAccessExpression(node) || !ts.isNonNullExpression(node.expression))
@@ -315,4 +373,12 @@ export class EmitterBase {
315
373
  if (createdBy && !this.created.has(a.name))
316
374
  this.fail(node, `"${a.name}" is used before ${a.name}.${createdBy}(...) runs`);
317
375
  }
376
+ /** The arguments and accounts code written here can name (what a closure written here keeps). */
377
+ envNow() {
378
+ return { args: new Map(this.args), accounts: new Map(this.accounts) };
379
+ }
380
+ /** None: a helper's or factory's body sees its own parameters and the module only. */
381
+ noEnv() {
382
+ return { args: new Map(), accounts: new Map() };
383
+ }
318
384
  }
@@ -1,6 +1,6 @@
1
1
  import ts from "typescript";
2
2
  import { type ErrorDef, type FieldType, type Helper } from "../model.js";
3
- import { type Value, type Ref } from "./shared.js";
3
+ import { type Value, type Ref, type Closure } from "./shared.js";
4
4
  import { ExpressionEmitter } from "./expressions.js";
5
5
  export declare abstract class CallEmitter extends ExpressionEmitter {
6
6
  protected call(node: ts.CallExpression, topLevel: boolean): Value | undefined;
@@ -31,6 +31,23 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
31
31
  protected loadAccount(node: ts.CallExpression, mut: boolean): Value;
32
32
  /** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
33
33
  private helperGrows;
34
+ /**
35
+ * A helper given a closure (a function that reads the caller's variables) for a function parameter: a closure only
36
+ * runs where it is declared, so the helper is expanded here, like a closure, with the closure bound to the
37
+ * parameter. Helpers expanded so take values (not accounts) and have no type parameters.
38
+ */
39
+ protected helperWithClosure(node: ts.CallExpression): {
40
+ value: Value | undefined;
41
+ } | undefined;
42
+ /** The function a function returns (`return (x) => x + n`, or an arrow body `(n) => (x) => x + n`), if it returns one. */
43
+ protected returnedFunction(fn: Closure["node"]): ts.ArrowFunction | ts.FunctionExpression | undefined;
44
+ /**
45
+ * `adder(5n)`, where `adder` returns a function: the arguments are evaluated here into locals of the current block,
46
+ * and the function it returns is bound to them as a closure (expanded where it is called, like any closure).
47
+ */
48
+ protected madeFunction(call: ts.CallExpression): Closure | undefined;
49
+ /** A function given for a function parameter of an expanded helper or closure, as something to expand. */
50
+ protected closureOf(arg: ts.Expression): Closure;
34
51
  protected callHelper(node: ts.CallExpression, h: Helper): Value | undefined;
35
52
  /** One helper argument: a C argument list fragment (accounts, seeds, references) or a value to settle. */
36
53
  protected helperArg(h: Helper, p: Helper["params"][number], n: ts.Expression): Value | string;