@typesol/cli 0.23.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.
- package/CHANGELOG.md +61 -0
- package/README.md +13 -12
- package/SECURITY.md +1 -1
- package/dist/clientgen.js +190 -41
- package/dist/compiler/analyze.js +111 -19
- package/dist/compiler/emit/base.d.ts +17 -4
- package/dist/compiler/emit/base.js +60 -15
- package/dist/compiler/emit/calls.d.ts +7 -0
- package/dist/compiler/emit/calls.js +99 -9
- package/dist/compiler/emit/cpi.d.ts +23 -0
- package/dist/compiler/emit/cpi.js +232 -1
- package/dist/compiler/emit/expressions.d.ts +9 -1
- package/dist/compiler/emit/expressions.js +16 -12
- package/dist/compiler/emit/function.js +3 -1
- package/dist/compiler/emit/shared.d.ts +11 -1
- package/dist/compiler/emit/statements.d.ts +4 -2
- package/dist/compiler/emit/statements.js +128 -27
- package/dist/compiler/emit/storage.d.ts +6 -2
- package/dist/compiler/emit/storage.js +36 -32
- package/dist/compiler/model.d.ts +8 -0
- package/dist/idlpublish.d.ts +6 -1
- package/dist/idlpublish.js +47 -5
- package/dist/portgen.js +1 -1
- package/dist/svmrpc.d.ts +7 -1
- package/dist/svmrpc.js +133 -19
- package/dist/testing.js +23 -3
- package/docs/cli.md +2 -2
- package/docs/client.md +13 -10
- package/docs/errors.md +9 -3
- package/docs/language.md +18 -17
- package/docs/mainnet.md +1 -1
- package/docs/migrate-from-anchor.md +8 -9
- package/docs/test-your-program.md +6 -2
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +3 -2
- package/lib/typesol.d.ts +120 -21
- package/package.json +1 -1
- package/runtime/typesol_rt.h +54 -18
package/dist/compiler/analyze.js
CHANGED
|
@@ -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
|
|
428
|
-
if (
|
|
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 (
|
|
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,6 +673,7 @@ export function analyze(entry) {
|
|
|
595
673
|
generics,
|
|
596
674
|
helperDecls,
|
|
597
675
|
closureDecls,
|
|
676
|
+
factories,
|
|
598
677
|
errors,
|
|
599
678
|
errorEnums,
|
|
600
679
|
enums,
|
|
@@ -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
|
|
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}
|
|
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,24 @@ 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
|
+
/** 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
|
+
}
|
|
1088
1180
|
function captures(l, localDecls, checker) {
|
|
1089
1181
|
const fn = l.node;
|
|
1090
1182
|
let found = false;
|
|
@@ -1652,7 +1744,7 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1652
1744
|
if (!s)
|
|
1653
1745
|
reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
|
|
1654
1746
|
if (s.vec)
|
|
1655
|
-
reporter.fail(node, `${s.name}
|
|
1747
|
+
reporter.fail(node, `${s.name} has a growable Vec; Zeroed<T> is for fixed-size (large) accounts`);
|
|
1656
1748
|
if (s.layout === "borsh")
|
|
1657
1749
|
reporter.fail(node, `${s.name} is @layout borsh; Zeroed<T> reads large accounts in place, so it takes fixed layouts only`);
|
|
1658
1750
|
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";
|
|
@@ -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
|
-
|
|
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). */
|
|
@@ -61,7 +64,7 @@ export declare abstract class EmitterBase {
|
|
|
61
64
|
private structArg;
|
|
62
65
|
private fixedStruct;
|
|
63
66
|
/** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
|
|
64
|
-
|
|
67
|
+
protected typeArgsOf(node: ts.CallExpression, g: GenericHelper): TypeArg[];
|
|
65
68
|
protected line(s: string): void;
|
|
66
69
|
/** `file.ts:line` of a node, for messages. */
|
|
67
70
|
protected location(node: ts.Node): string;
|
|
@@ -78,11 +81,16 @@ export declare abstract class EmitterBase {
|
|
|
78
81
|
protected noOptionalChain(node: ts.Node): void;
|
|
79
82
|
protected lookup(name: string): Local | undefined;
|
|
80
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;
|
|
81
89
|
protected fail(node: ts.Node, message: string): never;
|
|
82
90
|
protected exportName(node: ts.Expression): string | undefined;
|
|
83
91
|
/**
|
|
84
92
|
* `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
|
|
85
|
-
* failing with
|
|
93
|
+
* failing with 5115 otherwise). Returns undefined for anything else.
|
|
86
94
|
*/
|
|
87
95
|
protected withoutBang(node: ts.Expression): ts.PropertyAccessExpression | undefined;
|
|
88
96
|
protected accountParam(node: ts.Expression, what: string): AccountParam;
|
|
@@ -120,6 +128,11 @@ export declare abstract class EmitterBase {
|
|
|
120
128
|
protected abstract listMethod(node: ts.CallExpression, method: string): Value | undefined;
|
|
121
129
|
protected abstract isPipeline(e: ts.Expression): e is ts.CallExpression;
|
|
122
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;
|
|
123
136
|
protected abstract eachElement(list: ts.Expression, at: ts.Node, names: {
|
|
124
137
|
index: string;
|
|
125
138
|
ci: string;
|
|
@@ -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;
|
|
@@ -172,21 +173,12 @@ export class EmitterBase {
|
|
|
172
173
|
if (!arg || !p.type || !ts.isTypeReferenceNode(p.type) || !ts.isIdentifier(p.type.typeName) || p.type.typeName.text !== tp || p.type.typeArguments)
|
|
173
174
|
continue;
|
|
174
175
|
// Type the argument without emitting it: it is lowered again, once, by the call.
|
|
175
|
-
const
|
|
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;
|
|
176
|
+
const probe = this.probe(() => this.expr(arg));
|
|
181
177
|
if (probe?.type.kind === "structRef")
|
|
182
178
|
return this.fixedStruct(probe.type.struct, arg);
|
|
183
179
|
if (!probe) {
|
|
184
180
|
// A struct place (`p.a`) is not a value: type it as a reference, again without emitting anything.
|
|
185
|
-
const r = this.
|
|
186
|
-
this.lines.length = saved.lines;
|
|
187
|
-
this.hoisted.length = saved.hoisted;
|
|
188
|
-
this.temp = saved.temp;
|
|
189
|
-
this.reporter.diagnostics.length = saved.diagnostics;
|
|
181
|
+
const r = this.probe(() => this.ref(arg));
|
|
190
182
|
if (r?.type.kind === "struct")
|
|
191
183
|
return this.fixedStruct(r.type.name, arg);
|
|
192
184
|
}
|
|
@@ -283,9 +275,54 @@ export class EmitterBase {
|
|
|
283
275
|
block(fn) {
|
|
284
276
|
this.scopes.push(new Map());
|
|
285
277
|
this.indent++;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
+
}
|
|
289
326
|
}
|
|
290
327
|
fail(node, message) {
|
|
291
328
|
return this.reporter.fail(node, message);
|
|
@@ -295,7 +332,7 @@ export class EmitterBase {
|
|
|
295
332
|
}
|
|
296
333
|
/**
|
|
297
334
|
* `acct!.field` on an optional account: the same access without `!` (using the account checks that it was passed,
|
|
298
|
-
* failing with
|
|
335
|
+
* failing with 5115 otherwise). Returns undefined for anything else.
|
|
299
336
|
*/
|
|
300
337
|
withoutBang(node) {
|
|
301
338
|
if (!ts.isPropertyAccessExpression(node) || !ts.isNonNullExpression(node.expression))
|
|
@@ -336,4 +373,12 @@ export class EmitterBase {
|
|
|
336
373
|
if (createdBy && !this.created.has(a.name))
|
|
337
374
|
this.fail(node, `"${a.name}" is used before ${a.name}.${createdBy}(...) runs`);
|
|
338
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
|
+
}
|
|
339
384
|
}
|
|
@@ -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}
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
|
@@ -102,6 +105,26 @@ export declare abstract class CpiEmitter extends CallEmitter {
|
|
|
102
105
|
protected corePluginList(n: ts.Expression | undefined): MplField[];
|
|
103
106
|
/** Metaplex Core: assets and collections (with plugins), plugins added, updated and removed, transfer and burn. */
|
|
104
107
|
protected coreOp(node: ts.CallExpression, core: AccountParam, method: string): undefined;
|
|
108
|
+
/** The fields of a Core object literal (`{ kind: "AppData", … }`) by name. */
|
|
109
|
+
private coreProps;
|
|
110
|
+
/** PluginAuthority: "none" | "owner" | "updateAuthority" (literals), or an address. */
|
|
111
|
+
private coreAuthority;
|
|
112
|
+
private coreOption;
|
|
113
|
+
private coreSchema;
|
|
114
|
+
/** Vec<(HookableLifecycleEvent, ExternalCheckResult)>: `[{ event: "transfer", canReject: true }, …]`. */
|
|
115
|
+
private coreChecks;
|
|
116
|
+
/** ExtraAccount: `{ kind: "address", address, isSigner?, isWritable? }` or a preconfigured one (`{ kind: "owner" }`, …). */
|
|
117
|
+
private coreExtraAccount;
|
|
118
|
+
/**
|
|
119
|
+
* ExternalPluginAdapterInitInfo from a literal: `{ kind: "AppData", dataAuthority, schema? }`, `{ kind: "Oracle",
|
|
120
|
+
* baseAddress, checks, resultsOffset?, baseAddressConfig? }`, `{ kind: "LifecycleHook", hookedProgram, checks,
|
|
121
|
+
* extraAccounts?, dataAuthority?, schema? }` and the Linked forms (collections); `authority?` is the plugin's own.
|
|
122
|
+
*/
|
|
123
|
+
protected coreExternalPlugin(n: ts.Expression): MplField[];
|
|
124
|
+
/** ExternalPluginAdapterKey: `{ kind: "AppData", dataAuthority }`, `{ kind: "Oracle", address }`, `{ kind: "LifecycleHook", address }`, … */
|
|
125
|
+
protected coreExternalKey(n: ts.Expression): MplField[];
|
|
126
|
+
/** The data written: Option<Vec<u8>> from a string, a Vec<u8> argument, or null (clears it). */
|
|
127
|
+
protected coreExternalData(n: ts.Expression): MplField[];
|
|
105
128
|
/** PluginType (u8) from a kind string literal: removePlugin(…, "FreezeDelegate"). */
|
|
106
129
|
protected corePluginType(n: ts.Expression): MplField;
|
|
107
130
|
/** Bubblegum (compressed NFTs on SPL account compression): createTree, mint (MintV1, no collection, no creators). */
|