@typesol/cli 0.23.0 → 0.24.1

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 +77 -2
  2. package/README.md +18 -15
  3. package/SECURITY.md +1 -1
  4. package/dist/clientgen.js +222 -61
  5. package/dist/compiler/analyze.js +132 -20
  6. package/dist/compiler/emit/base.d.ts +27 -4
  7. package/dist/compiler/emit/base.js +68 -15
  8. package/dist/compiler/emit/calls.d.ts +7 -0
  9. package/dist/compiler/emit/calls.js +99 -9
  10. package/dist/compiler/emit/cpi.d.ts +33 -1
  11. package/dist/compiler/emit/cpi.js +353 -110
  12. package/dist/compiler/emit/expressions.d.ts +9 -1
  13. package/dist/compiler/emit/expressions.js +16 -12
  14. package/dist/compiler/emit/function.js +3 -1
  15. package/dist/compiler/emit/shared.d.ts +11 -1
  16. package/dist/compiler/emit/statements.d.ts +4 -2
  17. package/dist/compiler/emit/statements.js +128 -27
  18. package/dist/compiler/emit/storage.d.ts +6 -2
  19. package/dist/compiler/emit/storage.js +36 -32
  20. package/dist/compiler/model.d.ts +8 -0
  21. package/dist/idlpublish.d.ts +6 -1
  22. package/dist/idlpublish.js +47 -5
  23. package/dist/portgen.js +1 -1
  24. package/dist/svmrpc.d.ts +7 -1
  25. package/dist/svmrpc.js +133 -19
  26. package/dist/testing.js +23 -3
  27. package/docs/architecture.md +26 -2
  28. package/docs/cli.md +2 -2
  29. package/docs/client.md +13 -10
  30. package/docs/errors.md +9 -3
  31. package/docs/language.md +18 -17
  32. package/docs/mainnet.md +1 -1
  33. package/docs/migrate-from-anchor.md +8 -9
  34. package/docs/runtime.md +59 -0
  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 +120 -21
  39. package/package.json +1 -1
  40. package/runtime/typesol_rt.h +106 -28
@@ -424,24 +424,69 @@ export function analyze(entry) {
424
424
  }
425
425
  // An arrow written as a helper's function argument (`apply((x: u64): u64 => x + 1n, v)`): a local function of
426
426
  // its own, passed like a named one. Missing parameter and return types come from the helper's parameter type.
427
- const target = (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && ts.isCallExpression(n.parent) ? fnParamType(n, checker) : undefined;
428
- if (target) {
427
+ const found = (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && ts.isCallExpression(n.parent) ? fnParamType(n, checker) : undefined;
428
+ if (found) {
429
429
  reporter.guard(() => {
430
+ const { type: target, owner } = found;
430
431
  if (generic)
431
432
  reporter.fail(n, "arrow functions passed inside a generic helper are not supported; declare the function at the top level");
432
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
+ };
433
478
  lit.parameters.forEach((p, i) => {
434
- const from = target.parameters[i]?.type;
435
- if (!p.type && from)
479
+ const from = graft(p.type ? undefined : target.parameters[i]?.type, p);
480
+ if (from)
436
481
  p.type = from;
437
482
  });
438
483
  if (!lit.type)
439
- lit.type = target.type;
484
+ lit.type = graft(target.type, lit);
440
485
  let name = `${outer}__fn`;
441
486
  for (let i = 2; taken.has(name); i++)
442
487
  name = `${outer}__fn${i}`;
443
488
  taken.add(name);
444
- const src = { node: lit, name, display: "the function passed here", decl: lit, imports, outer: fn };
489
+ const src = { node: lit, name, display: "the function passed here", decl: lit, imports: lifted, outer: fn };
445
490
  locals.push(src);
446
491
  if (lit.body)
447
492
  collectLocals(lit.body, name, imports, lit);
@@ -452,6 +497,9 @@ export function analyze(entry) {
452
497
  };
453
498
  visit(body);
454
499
  };
500
+ const importsOf = new Map();
501
+ for (const f of [...functions, ...helperNodes])
502
+ importsOf.set(f.node.getSourceFile(), f.imports);
455
503
  for (const f of functions)
456
504
  if (f.node.body && f.node.name)
457
505
  collectLocals(f.node.body, f.node.name.text, f.imports, f.node);
@@ -474,8 +522,13 @@ export function analyze(entry) {
474
522
  const helpers = new Map();
475
523
  const generics = new Map();
476
524
  const helperDecls = new Map();
525
+ const factories = new Map();
477
526
  for (const fn of [...helperNodes, ...locals.filter((l) => !inline.has(l.decl))]) {
478
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
+ }
479
532
  const fnParams = fn.node.parameters.map((p, i) => (p.type && ts.isFunctionTypeNode(p.type) ? i : -1)).filter((i) => i >= 0);
480
533
  if (fn.node.typeParameters?.length || fnParams.length) {
481
534
  reporter.guard(() => {
@@ -545,14 +598,6 @@ export function analyze(entry) {
545
598
  if (structs.get(s).layout === "borsh" && s !== whole)
546
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`);
547
600
  }
548
- // Arguments are Borsh: a FixedVec's padded layout is account data only (a @layout borsh struct's FixedVec is a Borsh Vec).
549
- for (const a of ix.args) {
550
- const seen = new Set();
551
- collectStructs(a.type, structs, dataEnums, seen);
552
- for (const s of seen)
553
- if (structs.get(s).layout !== "borsh" && structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
554
- reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} holds a FixedVec, which only account data can; make ${s} @layout borsh, or pass the list as its own FixedVec<T, N> argument`);
555
- }
556
601
  // A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
557
602
  if (ix.returns?.kind === "struct")
558
603
  collectStructs(ix.returns, structs, dataEnums, argStructs);
@@ -563,6 +608,39 @@ export function analyze(entry) {
563
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>`);
564
609
  }
565
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
+ }
566
644
  checkSnakeDuplicates(instructions.map((ix) => ({ name: ix.node.name.text, node: ix.node.name })), "instructions", reporter);
567
645
  // emitCpi's event instruction (Anchor's EVENT_IX_TAG) is dispatched before every other instruction.
568
646
  if (instructions.some((ix) => ix.accounts.some((a) => a.kind.kind === "eventAuthority"))) {
@@ -595,13 +673,14 @@ export function analyze(entry) {
595
673
  generics,
596
674
  helperDecls,
597
675
  closureDecls,
676
+ factories,
598
677
  errors,
599
678
  errorEnums,
600
679
  enums,
601
680
  dataEnums,
602
681
  options,
603
682
  constants,
604
- events: new Set(),
683
+ events: collectEvents([...functions, ...helperNodes], structs),
605
684
  accountStructs,
606
685
  argStructs,
607
686
  fstrings,
@@ -842,7 +921,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
842
921
  }
843
922
  if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && names.has(node.typeName.text) && !targs.length)
844
923
  return { kind: "struct", name: node.typeName.text };
845
- 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");
846
925
  };
847
926
  for (const { decl, imports: fileImports } of items) {
848
927
  scope = scopeFor(fileImports);
@@ -1012,7 +1091,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
1012
1091
  if (s.size === undefined)
1013
1092
  reporter.fail(at, `${t.name} has a string field and cannot be nested; use FixedString<N>`);
1014
1093
  if (s.vec)
1015
- 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`);
1016
1095
  return s.size;
1017
1096
  }
1018
1097
  }
@@ -1080,11 +1159,44 @@ function fnParamType(arrow, checker) {
1080
1159
  ? d.initializer
1081
1160
  : undefined;
1082
1161
  const t = fn?.parameters[i]?.type;
1083
- if (t && ts.isFunctionTypeNode(t))
1084
- return t;
1162
+ if (fn && t && ts.isFunctionTypeNode(t))
1163
+ return { type: t, owner: fn };
1085
1164
  }
1086
1165
  return undefined;
1087
1166
  }
1167
+ /**
1168
+ * The structs a program emits (`emit<T>(…)` / `emitCpi<T>(…)` anywhere in its functions and helpers), known before
1169
+ * any code is emitted, so the IDL and the event checks do not depend on the order code is emitted in.
1170
+ */
1171
+ function collectEvents(sources, structs) {
1172
+ const events = new Set();
1173
+ for (const { node, imports } of sources) {
1174
+ const visit = (n) => {
1175
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
1176
+ const name = imports.get(n.expression.text);
1177
+ const t = n.typeArguments?.[0];
1178
+ if ((name === "emit" || name === "emitCpi") && t && ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && structs.has(t.typeName.text))
1179
+ events.add(t.typeName.text);
1180
+ }
1181
+ ts.forEachChild(n, visit);
1182
+ };
1183
+ visit(node);
1184
+ }
1185
+ return events;
1186
+ }
1187
+ /** Whether a function returns a function: typed so, or its body is (or only returns) an arrow. */
1188
+ function returnsFunction(fn) {
1189
+ if (fn.type)
1190
+ return ts.isFunctionTypeNode(fn.type);
1191
+ const strip = (e) => (e && ts.isParenthesizedExpression(e) ? strip(e.expression) : e);
1192
+ const body = fn.body;
1193
+ const ret = body && !ts.isBlock(body)
1194
+ ? strip(body)
1195
+ : body?.statements.length === 1 && ts.isReturnStatement(body.statements[0])
1196
+ ? strip(body.statements[0].expression)
1197
+ : undefined;
1198
+ return !!ret && (ts.isArrowFunction(ret) || ts.isFunctionExpression(ret));
1199
+ }
1088
1200
  function captures(l, localDecls, checker) {
1089
1201
  const fn = l.node;
1090
1202
  let found = false;
@@ -1652,7 +1764,7 @@ function resolveAccountKind(node, reporter, imports, structs) {
1652
1764
  if (!s)
1653
1765
  reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
1654
1766
  if (s.vec)
1655
- reporter.fail(node, `${s.name} ends in a Vec; Zeroed<T> is for fixed-size (large) accounts`);
1767
+ reporter.fail(node, `${s.name} has a growable Vec; Zeroed<T> is for fixed-size (large) accounts`);
1656
1768
  if (s.layout === "borsh")
1657
1769
  reporter.fail(node, `${s.name} is @layout borsh; Zeroed<T> reads large accounts in place, so it takes fixed layouts only`);
1658
1770
  return { kind: "zeroed", struct: s };
@@ -1,5 +1,5 @@
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
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";
@@ -24,6 +24,16 @@ export declare abstract class EmitterBase {
24
24
  protected readonly pendingInit: Set<string>;
25
25
  /** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
26
26
  protected readonly bumps: Map<string, string>;
27
+ /**
28
+ * Seed arrays already declared (no spare slot, every seed read in place): the C entries, and where the array is.
29
+ * An identical list in the same or an inner block reuses it (see seeds()).
30
+ */
31
+ protected readonly seedArrays: Map<string, {
32
+ name: string;
33
+ scope: Map<string, Local>;
34
+ at: number;
35
+ text: string;
36
+ }>;
27
37
  /** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
28
38
  protected inlineFrames: {
29
39
  label: string;
@@ -32,6 +42,8 @@ export declare abstract class EmitterBase {
32
42
  declAt: number;
33
43
  pad: string;
34
44
  node: ts.Node;
45
+ /** A place the result is written to (a stored `map` element), instead of a value. */
46
+ into?: Ref;
35
47
  }[];
36
48
  /** Closures being expanded (a closure calling itself cannot be expanded). */
37
49
  protected readonly inlining: Set<ts.Node>;
@@ -39,7 +51,8 @@ export declare abstract class EmitterBase {
39
51
  protected readonly loops: string[];
40
52
  protected readonly ix: Instruction | undefined;
41
53
  protected readonly helper: Helper | undefined;
42
- protected readonly scope: TypeScope;
54
+ /** Type names in effect (a generic helper expanded where it is called binds its type parameters here). */
55
+ protected scope: TypeScope;
43
56
  protected readonly checker: ts.TypeChecker;
44
57
  protected readonly analysis: Analysis;
45
58
  /** Calls already resolved to a helper (a generic one's instance is found by probing its arguments). */
@@ -61,7 +74,7 @@ export declare abstract class EmitterBase {
61
74
  private structArg;
62
75
  private fixedStruct;
63
76
  /** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
64
- private typeArgsOf;
77
+ protected typeArgsOf(node: ts.CallExpression, g: GenericHelper): TypeArg[];
65
78
  protected line(s: string): void;
66
79
  /** `file.ts:line` of a node, for messages. */
67
80
  protected location(node: ts.Node): string;
@@ -78,11 +91,16 @@ export declare abstract class EmitterBase {
78
91
  protected noOptionalChain(node: ts.Node): void;
79
92
  protected lookup(name: string): Local | undefined;
80
93
  protected block(fn: () => void): void;
94
+ /**
95
+ * Run `fn` to learn something (a type, a storage kind) without keeping anything it did: the emitted and hoisted
96
+ * lines, temporaries, diagnostics and emitter state are put back. Undefined when `fn` fails.
97
+ */
98
+ protected probe<T>(fn: () => T): T | undefined;
81
99
  protected fail(node: ts.Node, message: string): never;
82
100
  protected exportName(node: ts.Expression): string | undefined;
83
101
  /**
84
102
  * `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
85
- * failing with 3020 otherwise). Returns undefined for anything else.
103
+ * failing with 5115 otherwise). Returns undefined for anything else.
86
104
  */
87
105
  protected withoutBang(node: ts.Expression): ts.PropertyAccessExpression | undefined;
88
106
  protected accountParam(node: ts.Expression, what: string): AccountParam;
@@ -120,6 +138,11 @@ export declare abstract class EmitterBase {
120
138
  protected abstract listMethod(node: ts.CallExpression, method: string): Value | undefined;
121
139
  protected abstract isPipeline(e: ts.Expression): e is ts.CallExpression;
122
140
  protected abstract closureOf(arg: ts.Expression): Closure;
141
+ /** The arguments and accounts code written here can name (what a closure written here keeps). */
142
+ protected envNow(): Closure["env"];
143
+ /** None: a helper's or factory's body sees its own parameters and the module only. */
144
+ protected noEnv(): Closure["env"];
145
+ protected abstract madeFunction(call: ts.CallExpression): Closure | undefined;
123
146
  protected abstract eachElement(list: ts.Expression, at: ts.Node, names: {
124
147
  index: string;
125
148
  ci: string;
@@ -23,6 +23,11 @@ export class EmitterBase {
23
23
  pendingInit = new Set();
24
24
  /** Bump temporaries from create()/createIfNeeded(), for `acct.pdaBump`. */
25
25
  bumps = new Map();
26
+ /**
27
+ * Seed arrays already declared (no spare slot, every seed read in place): the C entries, and where the array is.
28
+ * An identical list in the same or an inner block reuses it (see seeds()).
29
+ */
30
+ seedArrays = new Map();
26
31
  /** Closures being expanded, innermost last: a `return` inside one assigns `result` and jumps to `label`. */
27
32
  inlineFrames = [];
28
33
  /** Closures being expanded (a closure calling itself cannot be expanded). */
@@ -31,6 +36,7 @@ export class EmitterBase {
31
36
  loops = [];
32
37
  ix;
33
38
  helper;
39
+ /** Type names in effect (a generic helper expanded where it is called binds its type parameters here). */
34
40
  scope;
35
41
  checker;
36
42
  analysis;
@@ -172,21 +178,12 @@ export class EmitterBase {
172
178
  if (!arg || !p.type || !ts.isTypeReferenceNode(p.type) || !ts.isIdentifier(p.type.typeName) || p.type.typeName.text !== tp || p.type.typeArguments)
173
179
  continue;
174
180
  // Type the argument without emitting it: it is lowered again, once, by the call.
175
- const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
176
- const probe = this.reporter.guard(() => this.expr(arg));
177
- this.lines.length = saved.lines;
178
- this.hoisted.length = saved.hoisted;
179
- this.temp = saved.temp;
180
- this.reporter.diagnostics.length = saved.diagnostics;
181
+ const probe = this.probe(() => this.expr(arg));
181
182
  if (probe?.type.kind === "structRef")
182
183
  return this.fixedStruct(probe.type.struct, arg);
183
184
  if (!probe) {
184
185
  // A struct place (`p.a`) is not a value: type it as a reference, again without emitting anything.
185
- const r = this.reporter.guard(() => this.ref(arg));
186
- this.lines.length = saved.lines;
187
- this.hoisted.length = saved.hoisted;
188
- this.temp = saved.temp;
189
- this.reporter.diagnostics.length = saved.diagnostics;
186
+ const r = this.probe(() => this.ref(arg));
190
187
  if (r?.type.kind === "struct")
191
188
  return this.fixedStruct(r.type.name, arg);
192
189
  }
@@ -283,9 +280,57 @@ export class EmitterBase {
283
280
  block(fn) {
284
281
  this.scopes.push(new Map());
285
282
  this.indent++;
286
- fn();
287
- this.indent--;
288
- this.scopes.pop();
283
+ try {
284
+ fn();
285
+ }
286
+ finally {
287
+ this.indent--;
288
+ this.scopes.pop();
289
+ }
290
+ }
291
+ /**
292
+ * Run `fn` to learn something (a type, a storage kind) without keeping anything it did: the emitted and hoisted
293
+ * lines, temporaries, diagnostics and emitter state are put back. Undefined when `fn` fails.
294
+ */
295
+ probe(fn) {
296
+ const saved = {
297
+ lines: this.lines.length,
298
+ hoisted: this.hoisted.length, // a probed helper call may hoist declarations
299
+ temp: this.temp,
300
+ diagnostics: this.reporter.diagnostics.length,
301
+ scopes: this.scopes.length,
302
+ indent: this.indent,
303
+ loops: this.loops.length,
304
+ frames: this.inlineFrames.length,
305
+ created: new Set(this.created),
306
+ pendingInit: new Set(this.pendingInit),
307
+ bumps: new Map(this.bumps),
308
+ seedArrays: new Map(this.seedArrays),
309
+ };
310
+ try {
311
+ return this.reporter.guard(fn);
312
+ }
313
+ finally {
314
+ this.lines.length = saved.lines;
315
+ this.hoisted.length = saved.hoisted;
316
+ this.temp = saved.temp;
317
+ this.reporter.diagnostics.length = saved.diagnostics;
318
+ this.scopes.length = saved.scopes;
319
+ this.indent = saved.indent;
320
+ this.loops.length = saved.loops;
321
+ this.inlineFrames.length = saved.frames;
322
+ for (const [set, from] of [
323
+ [this.created, saved.created],
324
+ [this.pendingInit, saved.pendingInit],
325
+ ]) {
326
+ set.clear();
327
+ from.forEach((x) => set.add(x));
328
+ }
329
+ this.bumps.clear();
330
+ saved.bumps.forEach((v, k) => this.bumps.set(k, v));
331
+ this.seedArrays.clear();
332
+ saved.seedArrays.forEach((v, k) => this.seedArrays.set(k, v));
333
+ }
289
334
  }
290
335
  fail(node, message) {
291
336
  return this.reporter.fail(node, message);
@@ -295,7 +340,7 @@ export class EmitterBase {
295
340
  }
296
341
  /**
297
342
  * `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
298
- * failing with 3020 otherwise). Returns undefined for anything else.
343
+ * failing with 5115 otherwise). Returns undefined for anything else.
299
344
  */
300
345
  withoutBang(node) {
301
346
  if (!ts.isPropertyAccessExpression(node) || !ts.isNonNullExpression(node.expression))
@@ -336,4 +381,12 @@ export class EmitterBase {
336
381
  if (createdBy && !this.created.has(a.name))
337
382
  this.fail(node, `"${a.name}" is used before ${a.name}.${createdBy}(...) runs`);
338
383
  }
384
+ /** The arguments and accounts code written here can name (what a closure written here keeps). */
385
+ envNow() {
386
+ return { args: new Map(this.args), accounts: new Map(this.accounts) };
387
+ }
388
+ /** None: a helper's or factory's body sees its own parameters and the module only. */
389
+ noEnv() {
390
+ return { args: new Map(), accounts: new Map() };
391
+ }
339
392
  }
@@ -39,6 +39,13 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
39
39
  protected helperWithClosure(node: ts.CallExpression): {
40
40
  value: Value | undefined;
41
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;
42
49
  /** A function given for a function parameter of an expanded helper or closure, as something to expand. */
43
50
  protected closureOf(arg: ts.Expression): Closure;
44
51
  protected callHelper(node: ts.CallExpression, h: Helper): Value | undefined;
@@ -195,7 +195,7 @@ export class CallEmitter extends ExpressionEmitter {
195
195
  if (!isWritable(acct.kind) || !struct)
196
196
  this.fail(args[0], "realloc needs a Mut<Struct> account of this program");
197
197
  if (struct?.vec)
198
- this.fail(args[0], `${struct.name} ends in a Vec, which sizes the account itself (push/pop/clear)`);
198
+ this.fail(args[0], `${struct.name} has a growable Vec, which sizes the account itself (push/pop/clear)`);
199
199
  const size = this.expect(this.expr(args[1], U64), U64, args[1]);
200
200
  const payer = this.accountParam(args[2], "the payer");
201
201
  if (payer.kind.kind !== "signer" || !payer.kind.mut)
@@ -460,10 +460,21 @@ export class CallEmitter extends ExpressionEmitter {
460
460
  }
461
461
  if (builtin)
462
462
  this.fail(callee, `${builtin} cannot be called here`);
463
+ // `adder(5n)(1n)`: the function a function returns, called right away.
464
+ if (ts.isCallExpression(callee)) {
465
+ const made = this.madeFunction(callee);
466
+ if (made)
467
+ return this.expandClosure(made, node.arguments, node);
468
+ }
463
469
  if (ts.isIdentifier(callee)) {
464
470
  const local = this.lookup(callee.text);
471
+ if (local?.closure && this.returnedFunction(local.closure.node))
472
+ this.fail(node, `${callee.text} returns a function: bind it (const f = ${callee.text}(…)) or call it right away (${callee.text}(…)(…))`);
465
473
  if (local?.closure)
466
474
  return this.expandClosure(local.closure, node.arguments, node);
475
+ const name = calleeName(callee, this.model.helperDecls, this.checker) ?? callee.text;
476
+ if (!local && this.model.factories.has(name))
477
+ this.fail(node, `${callee.text} returns a function: bind it (const f = ${callee.text}(…)) or call it right away (${callee.text}(…)(…))`);
467
478
  }
468
479
  const inlined = this.helperWithClosure(node);
469
480
  if (inlined)
@@ -1007,17 +1018,96 @@ export class CallEmitter extends ExpressionEmitter {
1007
1018
  const g = this.givenClosure(node);
1008
1019
  if (!g)
1009
1020
  return undefined;
1010
- if (g.typeParams.length)
1011
- this.fail(node, `${g.display} has type parameters, so it takes only functions that read nothing around them; pass the values the function needs instead`);
1012
- for (const p of g.node.parameters)
1013
- if (p.type && !ts.isFunctionTypeNode(p.type) && !resolveValueType(p.type, this.scope))
1014
- this.fail(node, `${g.display} takes accounts or lists, so it cannot be expanded here for the function that reads the variables around it; pass the values it needs instead`);
1015
- return { value: this.expandClosure({ node: g.node, name: g.display, scopes: [] }, node.arguments, node) };
1021
+ // Type parameters: bound from the call (as an instance would be) while the body is expanded.
1022
+ const saved = this.scope;
1023
+ if (g.typeParams.length) {
1024
+ const typeArgs = this.typeArgsOf(node, g);
1025
+ const bound = new Map();
1026
+ const structs = new Map();
1027
+ g.typeParams.forEach((n, i) => {
1028
+ const t = typeArgs[i];
1029
+ if (t.kind === "struct")
1030
+ structs.set(n, t.name);
1031
+ else
1032
+ bound.set(n, t);
1033
+ });
1034
+ this.scope = { ...this.scope, typeArgs: bound, structArgs: structs };
1035
+ }
1036
+ try {
1037
+ return { value: this.expandClosure({ node: g.node, name: g.display, scopes: [], env: this.noEnv(), imports: g.imports }, node.arguments, node) };
1038
+ }
1039
+ finally {
1040
+ this.scope = saved;
1041
+ }
1042
+ }
1043
+ /** The function a function returns (`return (x) => x + n`, or an arrow body `(n) => (x) => x + n`), if it returns one. */
1044
+ returnedFunction(fn) {
1045
+ const strip = (e) => (e && ts.isParenthesizedExpression(e) ? strip(e.expression) : e);
1046
+ const body = fn.body;
1047
+ let ret;
1048
+ if (body && !ts.isBlock(body))
1049
+ ret = strip(body);
1050
+ else if (body && body.statements.length === 1 && ts.isReturnStatement(body.statements[0]))
1051
+ ret = strip(body.statements[0].expression);
1052
+ if (ret && (ts.isArrowFunction(ret) || ts.isFunctionExpression(ret)))
1053
+ return ret;
1054
+ if (fn.type && ts.isFunctionTypeNode(fn.type))
1055
+ this.fail(fn, "a function that returns a function has one statement: return (x) => … (the function it makes)");
1056
+ return undefined;
1057
+ }
1058
+ /**
1059
+ * `adder(5n)`, where `adder` returns a function: the arguments are evaluated here into locals of the current block,
1060
+ * and the function it returns is bound to them as a closure (expanded where it is called, like any closure).
1061
+ */
1062
+ madeFunction(call) {
1063
+ if (!ts.isIdentifier(call.expression))
1064
+ return undefined;
1065
+ const local = this.lookup(call.expression.text);
1066
+ let maker = local?.closure;
1067
+ if (!maker && !local) {
1068
+ const name = calleeName(call.expression, this.model.helperDecls, this.checker) ?? call.expression.text;
1069
+ const f = this.model.factories.get(name);
1070
+ if (f)
1071
+ maker = { node: f.node, name: call.expression.text, scopes: [], env: this.noEnv(), imports: f.imports };
1072
+ }
1073
+ if (!maker)
1074
+ return undefined;
1075
+ const made = this.returnedFunction(maker.node);
1076
+ if (!made)
1077
+ return undefined;
1078
+ const fn = maker.node;
1079
+ if (call.arguments.length !== fn.parameters.length)
1080
+ this.fail(call, `${maker.name}() takes ${fn.parameters.length} argument(s)`);
1081
+ const params = new Map();
1082
+ fn.parameters.forEach((p, i) => {
1083
+ if (!ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken)
1084
+ this.fail(p, "parameters are plain names");
1085
+ const arg = call.arguments[i];
1086
+ if (p.type && ts.isFunctionTypeNode(p.type)) {
1087
+ params.set(p.name.text, { c: "", type: BOOL, mutable: false, closure: this.closureOf(arg) });
1088
+ return;
1089
+ }
1090
+ const annotated = p.type ? resolveValueType(p.type, this.scope) : undefined;
1091
+ let v = this.expr(arg, annotated);
1092
+ if (v.literal !== undefined && !annotated)
1093
+ v = this.literal(v.literal, U64, arg);
1094
+ const type = annotated ?? v.type;
1095
+ if (!["int", "bool", "pubkey", "string", "enum", "option"].includes(type.kind))
1096
+ this.fail(arg, `the function made keeps integers, booleans, Pubkeys, strings, enums or Options, not ${typeName(type)}`);
1097
+ v = this.expect(v, type, arg);
1098
+ const c = `l_${p.name.text}_${this.temp++}`;
1099
+ if (type.kind === "pubkey")
1100
+ this.line(`SolPubkey ${c} = *(${v.c});`);
1101
+ else
1102
+ this.line(`${valueC(type)} ${c} = ${this.ownedString(v).c};`);
1103
+ params.set(p.name.text, { c, type, mutable: true });
1104
+ });
1105
+ return { node: made, name: `the function ${maker.name}() made`, scopes: [...maker.scopes, params], env: maker.env, imports: maker.imports };
1016
1106
  }
1017
1107
  /** A function given for a function parameter of an expanded helper or closure, as something to expand. */
1018
1108
  closureOf(arg) {
1019
1109
  if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
1020
- return { node: arg, name: "the function passed", scopes: [...this.scopes] };
1110
+ return { node: arg, name: "the function passed", scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
1021
1111
  if (ts.isIdentifier(arg)) {
1022
1112
  const l = this.lookup(arg.text);
1023
1113
  if (l?.closure)
@@ -1025,7 +1115,7 @@ export class CallEmitter extends ExpressionEmitter {
1025
1115
  const name = calleeName(arg, this.model.helperDecls, this.checker) ?? arg.text;
1026
1116
  const h = this.model.helpers.get(name) ?? this.model.generics.get(name);
1027
1117
  if (h)
1028
- return { node: h.node, name: h.display, scopes: [] };
1118
+ return { node: h.node, name: h.display, scopes: [], env: this.noEnv(), imports: h.imports };
1029
1119
  }
1030
1120
  this.fail(arg, "pass a function: its name, or an arrow written here");
1031
1121
  }
@@ -8,6 +8,9 @@ type MplField = {
8
8
  } | {
9
9
  value: ts.Expression;
10
10
  type: ValueType;
11
+ } | {
12
+ c: string;
13
+ n: string;
11
14
  };
12
15
  /**
13
16
  * Other programs: PDAs and seeds, account creation, System/Token/ATA/Metaplex CPIs, generic invoke, Borsh
@@ -100,8 +103,37 @@ export declare abstract class CpiEmitter extends CallEmitter {
100
103
  protected corePlugin(n: ts.Expression, withAuthority: boolean): MplField[];
101
104
  /** CreateV1 / CreateCollectionV1 plugins: Option<Vec<PluginAuthorityPair>>. */
102
105
  protected corePluginList(n: ts.Expression | undefined): MplField[];
103
- /** Metaplex Core: assets and collections (with plugins), plugins added, updated and removed, transfer and burn. */
106
+ /**
107
+ * Metaplex Core: assets and collections (with plugins), plugins and external plugin adapters added, updated, written
108
+ * and removed, transfer and burn. Each method is a row of CORE_OPS below: its accounts in order, its data.
109
+ */
104
110
  protected coreOp(node: ts.CallExpression, core: AccountParam, method: string): undefined;
111
+ /**
112
+ * Core instructions: `args` fixed arguments (then an optional plugin list when `plugins`, then optional signer
113
+ * seeds), accounts in order (`[argument, label, writable, signer]`, or "self": the Core program, Core's "none" for an
114
+ * optional account), at most `max` bytes of data.
115
+ */
116
+ private coreOps;
117
+ /** The fields of a Core object literal (`{ kind: "AppData", … }`) by name. */
118
+ private coreProps;
119
+ /** PluginAuthority: "none" | "owner" | "updateAuthority" (literals), or an address. */
120
+ private coreAuthority;
121
+ private coreOption;
122
+ private coreSchema;
123
+ /** Vec<(HookableLifecycleEvent, ExternalCheckResult)>: `[{ event: "transfer", canReject: true }, …]`. */
124
+ private coreChecks;
125
+ /** ExtraAccount: `{ kind: "address", address, isSigner?, isWritable? }` or a preconfigured one (`{ kind: "owner" }`, …). */
126
+ private coreExtraAccount;
127
+ /**
128
+ * ExternalPluginAdapterInitInfo from a literal: `{ kind: "AppData", dataAuthority, schema? }`, `{ kind: "Oracle",
129
+ * baseAddress, checks, resultsOffset?, baseAddressConfig? }`, `{ kind: "LifecycleHook", hookedProgram, checks,
130
+ * extraAccounts?, dataAuthority?, schema? }` and the Linked forms (collections); `authority?` is the plugin's own.
131
+ */
132
+ protected coreExternalPlugin(n: ts.Expression): MplField[];
133
+ /** ExternalPluginAdapterKey: `{ kind: "AppData", dataAuthority }`, `{ kind: "Oracle", address }`, `{ kind: "LifecycleHook", address }`, … */
134
+ protected coreExternalKey(n: ts.Expression): MplField[];
135
+ /** The data written: Option<Vec<u8>> from a string, a Vec<u8> argument, or null (clears it). */
136
+ protected coreExternalData(n: ts.Expression): MplField[];
105
137
  /** PluginType (u8) from a kind string literal: removePlugin(…, "FreezeDelegate"). */
106
138
  protected corePluginType(n: ts.Expression): MplField;
107
139
  /** Bubblegum (compressed NFTs on SPL account compression): createTree, mint (MintV1, no collection, no creators). */