@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.
- package/CHANGELOG.md +82 -0
- package/README.md +13 -12
- package/SECURITY.md +1 -1
- package/dist/clientgen.js +190 -41
- package/dist/compiler/analyze.js +195 -25
- package/dist/compiler/emit/base.d.ts +28 -5
- package/dist/compiler/emit/base.js +81 -15
- package/dist/compiler/emit/calls.d.ts +18 -1
- package/dist/compiler/emit/calls.js +159 -7
- package/dist/compiler/emit/cpi.d.ts +23 -0
- package/dist/compiler/emit/cpi.js +274 -5
- package/dist/compiler/emit/expressions.d.ts +9 -1
- package/dist/compiler/emit/expressions.js +25 -12
- package/dist/compiler/emit/function.js +8 -4
- package/dist/compiler/emit/program.js +9 -2
- package/dist/compiler/emit/shared.d.ts +43 -3
- package/dist/compiler/emit/shared.js +43 -0
- package/dist/compiler/emit/statements.d.ts +39 -2
- package/dist/compiler/emit/statements.js +346 -87
- package/dist/compiler/emit/storage.d.ts +6 -2
- package/dist/compiler/emit/storage.js +75 -45
- package/dist/compiler/model.d.ts +26 -1
- 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 +21 -20
- 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 +127 -20
- package/package.json +1 -1
- package/runtime/typesol_rt.h +73 -18
package/dist/compiler/analyze.js
CHANGED
|
@@ -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
|
-
|
|
646
|
-
|
|
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
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
if (
|
|
994
|
-
|
|
995
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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;
|