@typesol/cli 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -0
- package/README.md +11 -7
- package/dist/cli.js +147 -20
- package/dist/clientgen.js +37 -0
- package/dist/compiler/analyze.d.ts +11 -1
- package/dist/compiler/analyze.js +307 -37
- package/dist/compiler/emit/base.d.ts +7 -0
- package/dist/compiler/emit/base.js +62 -2
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +232 -0
- package/dist/compiler/emit/calls.js +50 -15
- package/dist/compiler/emit/cpi.d.ts +32 -8
- package/dist/compiler/emit/cpi.js +706 -25
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +126 -4
- package/dist/compiler/emit/function.js +45 -12
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +12 -0
- package/dist/compiler/emit/shared.js +22 -4
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +141 -7
- package/dist/compiler/emit/storage.d.ts +6 -1
- package/dist/compiler/emit/storage.js +82 -17
- package/dist/compiler/idl.js +61 -18
- package/dist/compiler/model.d.ts +56 -4
- package/dist/compiler/model.js +8 -2
- package/dist/compiler/toolchain.d.ts +2 -0
- package/dist/compiler/toolchain.js +27 -1
- package/dist/onchain.d.ts +30 -0
- package/dist/onchain.js +53 -0
- package/dist/portgen.d.ts +87 -0
- package/dist/portgen.js +367 -0
- package/dist/templates.js +49 -154
- package/dist/testing.d.ts +66 -0
- package/dist/testing.js +337 -0
- package/dist/workspace.js +11 -1
- package/docs/architecture.md +3 -0
- package/docs/cli.md +16 -2
- package/docs/errors.md +3 -2
- package/docs/language.md +19 -14
- package/docs/mainnet.md +5 -4
- package/docs/migrate-from-anchor.md +113 -0
- package/docs/test-your-program.md +52 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +7 -0
- package/lib/typesol.d.ts +325 -13
- package/package.json +19 -1
- package/runtime/typesol_rt.h +54 -9
- package/ts-plugin/index.js +112 -0
- package/ts-plugin/package.json +4 -0
package/dist/compiler/analyze.js
CHANGED
|
@@ -4,7 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { CompileError, Reporter } from "./diagnostics.js";
|
|
6
6
|
import { decodeAddress } from "../keys.js";
|
|
7
|
-
import {
|
|
7
|
+
import { borshIdentical } from "./emit/borsh.js";
|
|
8
|
+
import { optionKey, INT_INFO, INT_NAMES, scalarKey, fixedVecName, isScalar, scalarSize, typeName, } from "./model.js";
|
|
8
9
|
export const LIB_PATH = fileURLToPath(new URL("../../lib/typesol.d.ts", import.meta.url));
|
|
9
10
|
/** Names a struct field may not use because account parameters already expose them. */
|
|
10
11
|
const RESERVED_FIELDS = new Set(["key", "lamports", "dataLen", "create", "equals", "get", "set", "length", "isSigner", "isWritable", "pdaBump"]);
|
|
@@ -17,7 +18,7 @@ const C_RESERVED = new Set([
|
|
|
17
18
|
..."NULL SUCCESS HEAP_LENGTH HEAP_START_ADDRESS MAX_PERMITTED_DATA_INCREASE MAX_RETURN_DATA MAX_SEED_LENGTH_EXCEEDED INVALID_SEEDS SIZE_PUBKEY memcpy memset strlen static_assert offsetof sol_log_64".split(" "),
|
|
18
19
|
]);
|
|
19
20
|
/** Why a name cannot be used as a C member, or undefined when it can. */
|
|
20
|
-
function cNameProblem(name) {
|
|
21
|
+
export function cNameProblem(name) {
|
|
21
22
|
if (name.includes("__") || name.startsWith("_"))
|
|
22
23
|
return `"${name}": names starting with "_" or containing "__" are reserved for generated code`;
|
|
23
24
|
if (name.includes("$"))
|
|
@@ -57,6 +58,7 @@ const TYPE_EXPORTS = new Set([
|
|
|
57
58
|
"TokenInterface",
|
|
58
59
|
"FixedArray",
|
|
59
60
|
"FixedString",
|
|
61
|
+
"FixedVec",
|
|
60
62
|
"Program",
|
|
61
63
|
"UncheckedProgram",
|
|
62
64
|
"CpiAccount",
|
|
@@ -94,6 +96,8 @@ const MAX_ARRAY_LEN = 1_000_000;
|
|
|
94
96
|
const MAX_STRING_CAP = 10_240;
|
|
95
97
|
/** Solana's account size limit, and what a program can allocate itself through a CPI (Init<T>). */
|
|
96
98
|
export const MAX_ACCOUNT_SIZE = 10 * 1024 * 1024;
|
|
99
|
+
/** sol_set_return_data's limit. */
|
|
100
|
+
export const MAX_RETURN_DATA = 1024;
|
|
97
101
|
export const MAX_CPI_ALLOCATION = 10 * 1024;
|
|
98
102
|
export function snakeCase(name) {
|
|
99
103
|
return name
|
|
@@ -223,7 +227,7 @@ export function analyze(entry) {
|
|
|
223
227
|
const isExported = (n) => ts.canHaveModifiers(n) && (ts.getModifiers(n) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
224
228
|
const declare = (name, file) => {
|
|
225
229
|
// These prefixes name the IDL types TypeSOL generates for FixedString<N>, Option and data enums in account data.
|
|
226
|
-
if (/^(FixedString\d|FixedOption_|FixedEnum_)/.test(name.text))
|
|
230
|
+
if (/^(FixedString\d|FixedVec\d|FixedOption_|FixedEnum_)/.test(name.text))
|
|
227
231
|
reporter.fail(name, `"${name.text}" is reserved for IDL types TypeSOL generates; pick another name`);
|
|
228
232
|
const prev = declared.get(name.text);
|
|
229
233
|
if (prev)
|
|
@@ -311,7 +315,8 @@ export function analyze(entry) {
|
|
|
311
315
|
else {
|
|
312
316
|
if (stmt.name)
|
|
313
317
|
declare(stmt.name, file);
|
|
314
|
-
|
|
318
|
+
if (stmt.name)
|
|
319
|
+
helperNodes.push({ node: stmt, name: stmt.name.text, display: stmt.name.text, decl: stmt, imports });
|
|
315
320
|
}
|
|
316
321
|
}
|
|
317
322
|
else if (ts.isVariableStatement(stmt)) {
|
|
@@ -322,6 +327,14 @@ export function analyze(entry) {
|
|
|
322
327
|
if (!ts.isIdentifier(d.name) || !d.initializer)
|
|
323
328
|
reporter.fail(d, "declare const NAME = literal");
|
|
324
329
|
const init = d.initializer;
|
|
330
|
+
// `const f = (…): T => …` / `const f = function (…) {…}`: a helper.
|
|
331
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
332
|
+
if (isEntry && isExported(stmt))
|
|
333
|
+
reporter.fail(d.name, "instructions are function declarations: export function name(…)");
|
|
334
|
+
declare(d.name, file);
|
|
335
|
+
helperNodes.push({ node: init, name: d.name.text, display: d.name.text, decl: d, imports });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
325
338
|
let value;
|
|
326
339
|
if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && imports.get(init.expression.text) === "pubkey") {
|
|
327
340
|
const arg = init.arguments[0];
|
|
@@ -356,6 +369,7 @@ export function analyze(entry) {
|
|
|
356
369
|
}
|
|
357
370
|
}
|
|
358
371
|
const fstrings = new Set();
|
|
372
|
+
const fixedVecs = new Map();
|
|
359
373
|
const scopeFor = (imports) => ({ imports, enums, options, dataEnums });
|
|
360
374
|
for (const c of pendingConstTypes) {
|
|
361
375
|
reporter.guard(() => {
|
|
@@ -370,14 +384,87 @@ export function analyze(entry) {
|
|
|
370
384
|
c.def.type = t;
|
|
371
385
|
});
|
|
372
386
|
}
|
|
373
|
-
const structs = analyzeStructs(interfaces, aliases, dataEnums, reporter, scopeFor, fstrings);
|
|
387
|
+
const structs = analyzeStructs(interfaces, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs);
|
|
388
|
+
const checker = program.getTypeChecker();
|
|
389
|
+
// Local `const f = (…) => …` in any function body (and in those local functions): lifted to helpers.
|
|
390
|
+
const locals = [];
|
|
391
|
+
const taken = new Set();
|
|
392
|
+
const collectLocals = (body, outer, imports, fn) => {
|
|
393
|
+
const generic = !!fn.typeParameters?.length;
|
|
394
|
+
const visit = (n) => {
|
|
395
|
+
if (ts.isVariableDeclaration(n) && n.initializer && (ts.isArrowFunction(n.initializer) || ts.isFunctionExpression(n.initializer))) {
|
|
396
|
+
reporter.guard(() => {
|
|
397
|
+
if (!ts.isIdentifier(n.name))
|
|
398
|
+
reporter.fail(n.name, "name local functions: const f = (…) => …");
|
|
399
|
+
if (generic)
|
|
400
|
+
reporter.fail(n, "local functions inside a generic helper are not supported; declare this one at the top level");
|
|
401
|
+
if (!(n.parent.flags & ts.NodeFlags.Const) || !ts.isVariableStatement(n.parent.parent))
|
|
402
|
+
reporter.fail(n, "declare local functions with const: const f = (…) => …");
|
|
403
|
+
if (n.name.text.includes("__") || n.name.text.includes("$"))
|
|
404
|
+
reporter.fail(n.name, `"${n.name.text}": "__" and "$" are reserved in names`);
|
|
405
|
+
let name = `${outer}__${n.name.text}`;
|
|
406
|
+
for (let i = 2; taken.has(name); i++)
|
|
407
|
+
name = `${outer}__${n.name.text}__${i}`;
|
|
408
|
+
taken.add(name);
|
|
409
|
+
const src = { node: n.initializer, name, display: n.name.text, decl: n, imports, outer: fn };
|
|
410
|
+
locals.push(src);
|
|
411
|
+
if (src.node.body)
|
|
412
|
+
collectLocals(src.node.body, name, imports, src.node);
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
ts.forEachChild(n, visit);
|
|
417
|
+
};
|
|
418
|
+
visit(body);
|
|
419
|
+
};
|
|
420
|
+
for (const f of functions)
|
|
421
|
+
if (f.node.body && f.node.name)
|
|
422
|
+
collectLocals(f.node.body, f.node.name.text, f.imports, f.node);
|
|
423
|
+
for (const h of [...helperNodes])
|
|
424
|
+
if (h.node.body)
|
|
425
|
+
collectLocals(h.node.body, h.name, h.imports, h.node);
|
|
426
|
+
const localDecls = new Set(locals.map((l) => l.decl));
|
|
427
|
+
for (const l of locals)
|
|
428
|
+
reporter.guard(() => checkCaptures(l, localDecls, checker, reporter));
|
|
374
429
|
const helpers = new Map();
|
|
375
|
-
|
|
376
|
-
|
|
430
|
+
const generics = new Map();
|
|
431
|
+
const helperDecls = new Map();
|
|
432
|
+
for (const fn of [...helperNodes, ...locals]) {
|
|
433
|
+
helperDecls.set(fn.decl, fn.name);
|
|
434
|
+
if (fn.node.typeParameters?.length) {
|
|
435
|
+
reporter.guard(() => {
|
|
436
|
+
for (const tp of fn.node.typeParameters)
|
|
437
|
+
if (tp.default)
|
|
438
|
+
reporter.fail(tp, "type parameters of helpers take no default");
|
|
439
|
+
generics.set(fn.name, {
|
|
440
|
+
name: fn.name,
|
|
441
|
+
display: fn.display,
|
|
442
|
+
node: fn.node,
|
|
443
|
+
typeParams: fn.node.typeParameters.map((t) => t.name.text),
|
|
444
|
+
imports: fn.imports,
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const h = reporter.guard(() => analyzeHelper(fn.node, fn.name, fn.display, reporter, scopeFor(fn.imports), structs));
|
|
377
450
|
if (h)
|
|
378
451
|
helpers.set(h.name, h);
|
|
379
452
|
}
|
|
380
|
-
|
|
453
|
+
const instances = new Map();
|
|
454
|
+
const instantiate = (g, typeArgs) => {
|
|
455
|
+
const key = `${g.name}__${typeArgs.map(scalarKey).join("_")}`;
|
|
456
|
+
const done = instances.get(key);
|
|
457
|
+
if (done)
|
|
458
|
+
return done;
|
|
459
|
+
const bound = new Map(g.typeParams.map((n, i) => [n, typeArgs[i]]));
|
|
460
|
+
const display = `${g.display}<${typeArgs.map(typeName).join(", ")}>`;
|
|
461
|
+
const h = analyzeHelper(g.node, key, display, reporter, { ...scopeFor(g.imports), typeArgs: bound }, structs);
|
|
462
|
+
h.typeArgs = bound;
|
|
463
|
+
instances.set(key, h);
|
|
464
|
+
helpers.set(key, h);
|
|
465
|
+
return h;
|
|
466
|
+
};
|
|
467
|
+
reporter.guard(() => checkRecursion([...helperNodes, ...locals], helperDecls, checker, reporter));
|
|
381
468
|
const accountStructs = new Set();
|
|
382
469
|
const argStructs = new Set();
|
|
383
470
|
const instructions = [];
|
|
@@ -388,6 +475,26 @@ export function analyze(entry) {
|
|
|
388
475
|
instructions.push(ix);
|
|
389
476
|
for (const a of ix.args)
|
|
390
477
|
collectStructs(a.type, structs, dataEnums, argStructs);
|
|
478
|
+
for (const a of ix.args) {
|
|
479
|
+
const seen = new Set();
|
|
480
|
+
collectStructs(a.type, structs, dataEnums, seen);
|
|
481
|
+
for (const s of seen)
|
|
482
|
+
if (structs.get(s).layout === "borsh")
|
|
483
|
+
reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} is @layout borsh, an account layout only`);
|
|
484
|
+
}
|
|
485
|
+
if (ix.returns?.kind === "struct" && structs.get(ix.returns.name).layout === "borsh")
|
|
486
|
+
reporter.error(fn.node.type ?? fn.node, `${ix.returns.name} is @layout borsh, an account layout only; return another struct`);
|
|
487
|
+
// Arguments are Borsh: a FixedVec's padded layout is account data only.
|
|
488
|
+
for (const a of ix.args) {
|
|
489
|
+
const seen = new Set();
|
|
490
|
+
collectStructs(a.type, structs, dataEnums, seen);
|
|
491
|
+
for (const s of seen)
|
|
492
|
+
if (structs.get(s).fields.some((f) => f.type.kind === "vec" && f.type.cap !== undefined))
|
|
493
|
+
reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} holds a FixedVec, which only account data can; pass a Vec<T> argument instead`);
|
|
494
|
+
}
|
|
495
|
+
// A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
|
|
496
|
+
if (ix.returns?.kind === "struct")
|
|
497
|
+
collectStructs(ix.returns, structs, dataEnums, argStructs);
|
|
391
498
|
for (const a of ix.accounts)
|
|
392
499
|
if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded")
|
|
393
500
|
accountStructs.add(a.kind.struct.name);
|
|
@@ -421,6 +528,8 @@ export function analyze(entry) {
|
|
|
421
528
|
structs,
|
|
422
529
|
instructions,
|
|
423
530
|
helpers,
|
|
531
|
+
generics,
|
|
532
|
+
helperDecls,
|
|
424
533
|
errors,
|
|
425
534
|
errorEnums,
|
|
426
535
|
enums,
|
|
@@ -431,12 +540,14 @@ export function analyze(entry) {
|
|
|
431
540
|
accountStructs,
|
|
432
541
|
argStructs,
|
|
433
542
|
fstrings,
|
|
543
|
+
fixedVecs,
|
|
434
544
|
},
|
|
435
545
|
sourceFile,
|
|
436
546
|
files: programFiles(program, entryPath).map((f) => f.fileName),
|
|
437
547
|
imports: entryImports,
|
|
438
548
|
reporter,
|
|
439
|
-
checker
|
|
549
|
+
checker,
|
|
550
|
+
instantiate,
|
|
440
551
|
};
|
|
441
552
|
}
|
|
442
553
|
/** Every struct an argument type contains (transitively), for IDL layout decisions. */
|
|
@@ -510,6 +621,11 @@ export function typesolName(node, imports) {
|
|
|
510
621
|
export function resolveScalarType(node, scope) {
|
|
511
622
|
if (node.kind === ts.SyntaxKind.BooleanKeyword)
|
|
512
623
|
return { kind: "bool" };
|
|
624
|
+
if (scope.typeArgs && ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && !node.typeArguments) {
|
|
625
|
+
const t = scope.typeArgs.get(node.typeName.text);
|
|
626
|
+
if (t)
|
|
627
|
+
return t;
|
|
628
|
+
}
|
|
513
629
|
const n = typesolName(node, scope.imports);
|
|
514
630
|
if (n && INT_NAMES.includes(n))
|
|
515
631
|
return { kind: "int", name: n };
|
|
@@ -567,7 +683,7 @@ function numericLiteralArg(node) {
|
|
|
567
683
|
* Resolve every exported interface. Field types may name other interfaces, so names are
|
|
568
684
|
* collected first; sizes are computed after, rejecting cycles.
|
|
569
685
|
*/
|
|
570
|
-
function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings) {
|
|
686
|
+
function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs) {
|
|
571
687
|
const names = new Set(items.map((d) => d.decl.name.text));
|
|
572
688
|
const enumNames = new Set(aliases.map((a) => a.decl.name.text));
|
|
573
689
|
const structs = new Map();
|
|
@@ -623,6 +739,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
623
739
|
reporter.fail(targs[1], `array length must be 1..${MAX_ARRAY_LEN}`);
|
|
624
740
|
return { kind: "array", elem, len };
|
|
625
741
|
}
|
|
742
|
+
if (ext === "FixedVec") {
|
|
743
|
+
if (!targs[0])
|
|
744
|
+
reporter.fail(node, "FixedVec<T, N> needs an element type");
|
|
745
|
+
const elem = fieldType(targs[0]);
|
|
746
|
+
if (elem.kind === "string" || elem.kind === "vec")
|
|
747
|
+
reporter.fail(targs[0], "FixedVec elements must have a fixed size (use FixedString<N>)");
|
|
748
|
+
const cap = numericLiteralArg(targs[1]);
|
|
749
|
+
if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_ARRAY_LEN)
|
|
750
|
+
reporter.fail(node, `FixedVec<T, N> needs a literal capacity 1..${MAX_ARRAY_LEN}, e.g. FixedVec<u64, 16>`);
|
|
751
|
+
const t = { kind: "vec", elem, cap };
|
|
752
|
+
fixedVecs.set(fixedVecName(t), t);
|
|
753
|
+
return t;
|
|
754
|
+
}
|
|
626
755
|
if (ext === "FixedString") {
|
|
627
756
|
const cap = numericLiteralArg(targs[0]);
|
|
628
757
|
if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_STRING_CAP)
|
|
@@ -632,7 +761,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
632
761
|
}
|
|
633
762
|
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && names.has(node.typeName.text) && !targs.length)
|
|
634
763
|
return { kind: "struct", name: node.typeName.text };
|
|
635
|
-
reporter.fail(node, "field type must be an integer, boolean, Pubkey, FixedArray<T, N>, FixedString<N>, string (events only) or another struct");
|
|
764
|
+
reporter.fail(node, "field type must be an integer, boolean, Pubkey, FixedArray<T, N>, FixedString<N>, FixedVec<T, N>, Vec<T> (last), string (events only) or another struct");
|
|
636
765
|
};
|
|
637
766
|
for (const { decl, imports: fileImports } of items) {
|
|
638
767
|
scope = scopeFor(fileImports);
|
|
@@ -653,12 +782,17 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
653
782
|
if (problem)
|
|
654
783
|
reporter.fail(m.name, problem);
|
|
655
784
|
const type = fieldType(m.type);
|
|
656
|
-
if (type.kind === "vec" && m !== decl.members[decl.members.length - 1])
|
|
657
|
-
reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size)");
|
|
785
|
+
if (type.kind === "vec" && type.cap === undefined && m !== decl.members[decl.members.length - 1])
|
|
786
|
+
reporter.fail(m, "a Vec<T> field must be the last field of the struct (its length changes the account size); use FixedVec<T, N> for a list of at most N elsewhere");
|
|
658
787
|
fields.push({ name, type });
|
|
659
788
|
}
|
|
660
789
|
checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
|
|
661
790
|
const name = decl.name.text;
|
|
791
|
+
// `/** @layout borsh */`: stored as Anchor stores it.
|
|
792
|
+
const layoutTag = ts.getJSDocTags(decl).find((t) => t.tagName.text === "layout");
|
|
793
|
+
const layoutText = typeof layoutTag?.comment === "string" ? layoutTag.comment.trim() : undefined;
|
|
794
|
+
if (layoutTag && layoutText !== "borsh" && layoutText !== "fixed")
|
|
795
|
+
reporter.fail(layoutTag, "@layout is borsh (Anchor's account bytes) or fixed (the default)");
|
|
662
796
|
structs.set(name, {
|
|
663
797
|
name,
|
|
664
798
|
fields,
|
|
@@ -666,6 +800,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
666
800
|
accountDiscriminator: discriminator(`account:${name}`),
|
|
667
801
|
eventDiscriminator: discriminator(`event:${name}`),
|
|
668
802
|
node: decl,
|
|
803
|
+
layout: layoutText === "borsh" ? "borsh" : "fixed",
|
|
669
804
|
});
|
|
670
805
|
});
|
|
671
806
|
}
|
|
@@ -696,8 +831,8 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
696
831
|
if (problem)
|
|
697
832
|
reporter.fail(p.name, problem);
|
|
698
833
|
const t = fieldType(p.type);
|
|
699
|
-
if (t.kind === "
|
|
700
|
-
reporter.fail(p.type, "variant fields must have a fixed size (use
|
|
834
|
+
if (t.kind === "vec")
|
|
835
|
+
reporter.fail(p.type, "variant fields must have a fixed size (a Vec cannot be one; use FixedArray<T, N>)");
|
|
701
836
|
fields.push({ name: p.name.text, type: t });
|
|
702
837
|
}
|
|
703
838
|
if (!vname || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(vname))
|
|
@@ -734,15 +869,41 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
734
869
|
reporter.fail(at, `data enum ${e.name} contains itself`);
|
|
735
870
|
if (state.get(`enum:${e.name}`) !== "done") {
|
|
736
871
|
state.set(`enum:${e.name}`, "visiting");
|
|
872
|
+
// A string field is a TS_Str view (16 bytes) in the C payload and makes the enum variable-size.
|
|
873
|
+
const fieldSize = (f) => {
|
|
874
|
+
if (f.kind === "string") {
|
|
875
|
+
e.variable = true;
|
|
876
|
+
return 16;
|
|
877
|
+
}
|
|
878
|
+
if (f.kind === "dataEnum") {
|
|
879
|
+
const inner = dataEnums.get(f.name);
|
|
880
|
+
const size = sizeOf(f, e.node);
|
|
881
|
+
if (inner?.variable) {
|
|
882
|
+
e.variable = true;
|
|
883
|
+
return 1 + inner.payload;
|
|
884
|
+
}
|
|
885
|
+
return size;
|
|
886
|
+
}
|
|
887
|
+
return sizeOf(f, e.node);
|
|
888
|
+
};
|
|
737
889
|
for (const v of e.variants)
|
|
738
|
-
v.size = v.fields.reduce((sum, f) => sum +
|
|
890
|
+
v.size = v.fields.reduce((sum, f) => sum + fieldSize(f.type), 0);
|
|
739
891
|
e.payload = Math.max(0, ...e.variants.map((v) => v.size));
|
|
740
892
|
state.set(`enum:${e.name}`, "done");
|
|
741
893
|
}
|
|
894
|
+
// Variable enums have no fixed layout: a struct holding one is event-only, like one holding a string.
|
|
895
|
+
if (e.variable)
|
|
896
|
+
return undefined;
|
|
742
897
|
return 1 + e.payload;
|
|
743
898
|
}
|
|
744
|
-
case "vec":
|
|
745
|
-
|
|
899
|
+
case "vec": {
|
|
900
|
+
if (t.cap === undefined)
|
|
901
|
+
return 4; // the u32 length; elements live past the fixed part
|
|
902
|
+
const e = sizeOf(t.elem, at);
|
|
903
|
+
if (e === undefined)
|
|
904
|
+
reporter.fail(at, "FixedVec elements must have a fixed size");
|
|
905
|
+
return 4 + e * t.cap;
|
|
906
|
+
}
|
|
746
907
|
case "string":
|
|
747
908
|
return undefined;
|
|
748
909
|
case "fstring":
|
|
@@ -781,7 +942,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
781
942
|
if (total !== undefined && total + 8 > MAX_ACCOUNT_SIZE)
|
|
782
943
|
reporter.fail(s.node.name, `${s.name} is ${total + 8} bytes with its discriminator; Solana accounts are at most ${MAX_ACCOUNT_SIZE} bytes`);
|
|
783
944
|
const last = s.fields.at(-1);
|
|
784
|
-
if (last?.type.kind === "vec") {
|
|
945
|
+
if (last?.type.kind === "vec" && last.type.cap === undefined) {
|
|
785
946
|
const elemSize = sizeOf(last.type.elem, s.node);
|
|
786
947
|
if (elemSize === undefined)
|
|
787
948
|
reporter.fail(s.node, "Vec elements must have a fixed size");
|
|
@@ -791,20 +952,87 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
791
952
|
};
|
|
792
953
|
for (const s of structs.values())
|
|
793
954
|
reporter.guard(() => visit(s));
|
|
955
|
+
for (const s of structs.values())
|
|
956
|
+
if (s.layout === "borsh")
|
|
957
|
+
reporter.guard(() => checkBorshLayout(s, structs, reporter));
|
|
794
958
|
for (const e of dataEnums.values())
|
|
795
959
|
reporter.guard(() => sizeOf({ kind: "dataEnum", name: e.name }, e.node));
|
|
796
960
|
return structs;
|
|
797
961
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
962
|
+
const isFunctionNode = (n) => ts.isFunctionDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isMethodDeclaration(n);
|
|
963
|
+
/**
|
|
964
|
+
* A local function becomes a helper of its own, so it can only use its parameters, its own locals, top-level names and
|
|
965
|
+
* other local functions: reading a variable of the enclosing function (a closure) is refused.
|
|
966
|
+
*/
|
|
967
|
+
function checkCaptures(l, localDecls, checker, reporter) {
|
|
968
|
+
const fn = l.node;
|
|
969
|
+
const visit = (n) => {
|
|
970
|
+
if (ts.isIdentifier(n)) {
|
|
971
|
+
const sym = ts.isShorthandPropertyAssignment(n.parent) ? checker.getShorthandAssignmentValueSymbol(n.parent) : checker.getSymbolAtLocation(n);
|
|
972
|
+
for (const d of sym?.declarations ?? []) {
|
|
973
|
+
if (!(ts.isVariableDeclaration(d) || ts.isParameter(d) || ts.isBindingElement(d)) || localDecls.has(d))
|
|
974
|
+
continue;
|
|
975
|
+
if (d.getSourceFile() !== fn.getSourceFile() || (d.pos >= fn.pos && d.end <= fn.end))
|
|
976
|
+
continue;
|
|
977
|
+
// Declared in some function around this one: a captured variable.
|
|
978
|
+
for (let p = d.parent; p; p = p.parent)
|
|
979
|
+
if (isFunctionNode(p)) {
|
|
980
|
+
reporter.fail(n, `${l.display} reads "${n.text}" of the function around it; pass it as a parameter (local functions capture nothing)`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
ts.forEachChild(n, visit);
|
|
985
|
+
};
|
|
986
|
+
if (fn.body)
|
|
987
|
+
visit(fn.body);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* A `@layout borsh` struct: its own fields may be FixedString (Borsh String), FixedVec (Borsh Vec) and Option; anything
|
|
991
|
+
* nested must already be Borsh-identical (fixed-size scalars, Pubkeys, enums, arrays and structs of them).
|
|
992
|
+
*/
|
|
993
|
+
function checkBorshLayout(s, structs, reporter) {
|
|
994
|
+
const model = { structs };
|
|
995
|
+
const fixed = (t) => borshIdentical(t, model);
|
|
996
|
+
for (const f of s.fields) {
|
|
997
|
+
const t = f.type;
|
|
998
|
+
const at = s.node.members.find((m) => m.name && ts.isIdentifier(m.name) && m.name.text === f.name) ?? s.node;
|
|
999
|
+
if (fixed(t) || t.kind === "fstring")
|
|
1000
|
+
continue;
|
|
1001
|
+
if (t.kind === "vec" && t.cap === undefined)
|
|
1002
|
+
reporter.fail(at, "a @layout borsh struct cannot end in a growable Vec<T>; use FixedVec<T, N> (Anchor's #[max_len(N)] Vec)");
|
|
1003
|
+
if (t.kind === "vec" && (fixed(t.elem) || t.elem.kind === "fstring"))
|
|
1004
|
+
continue;
|
|
1005
|
+
if (t.kind === "option" && fixed(t.inner))
|
|
1006
|
+
continue;
|
|
1007
|
+
if (t.kind === "struct" && structs.get(t.name)?.layout === "borsh")
|
|
1008
|
+
reporter.fail(at, `${t.name} is @layout borsh itself: only account structs take a layout; nest a struct without one`);
|
|
1009
|
+
reporter.fail(at, `${typeName(t)} in a @layout borsh struct: its fields may be fixed-size values, FixedString<N>, FixedVec<T, N> (of those or of FixedString) and Option<T>; nested structs and lists hold fixed-size values only`);
|
|
1010
|
+
}
|
|
1011
|
+
for (const other of structs.values())
|
|
1012
|
+
for (const f of other.fields) {
|
|
1013
|
+
const inner = f.type.kind === "array" || f.type.kind === "vec" ? f.type.elem : f.type.kind === "option" ? f.type.inner : f.type;
|
|
1014
|
+
if (inner.kind === "struct" && inner.name === s.name)
|
|
1015
|
+
reporter.error(s.node.name, `${s.name} is @layout borsh, so it is account data only; ${other.name}.${f.name} cannot hold one`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function analyzeHelper(fn, name, display, reporter, scope, structs) {
|
|
1019
|
+
if (!fn.body)
|
|
1020
|
+
reporter.fail(fn, "helper functions need a body");
|
|
1021
|
+
if (fn.asteriskToken)
|
|
1022
|
+
reporter.fail(fn, "helper functions cannot be generators");
|
|
1023
|
+
if ((ts.getModifiers(fn) ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword))
|
|
1024
|
+
reporter.fail(fn, "helper functions cannot be async");
|
|
803
1025
|
if (!fn.type)
|
|
804
|
-
reporter.fail(fn.name, "helper functions need an explicit return type (void, an integer, boolean or
|
|
805
|
-
|
|
806
|
-
if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns)
|
|
807
|
-
|
|
1026
|
+
reporter.fail(fn.name ?? fn, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
|
|
1027
|
+
let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword ? undefined : resolveScalarType(fn.type, scope);
|
|
1028
|
+
if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns) {
|
|
1029
|
+
const s = ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments ? structs.get(fn.type.typeName.text) : undefined;
|
|
1030
|
+
if (s && (s.size === undefined || s.vec))
|
|
1031
|
+
reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be returned; return a struct of fixed size`);
|
|
1032
|
+
if (!s)
|
|
1033
|
+
reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, a struct or a type parameter");
|
|
1034
|
+
returns = { kind: "struct", name: s.name };
|
|
1035
|
+
}
|
|
808
1036
|
const params = fn.parameters.map((p) => {
|
|
809
1037
|
if (!ts.isIdentifier(p.name) || !p.type || p.initializer || p.questionToken || p.dotDotDotToken)
|
|
810
1038
|
reporter.fail(p, "parameters need a plain name and an explicit type");
|
|
@@ -825,16 +1053,43 @@ function analyzeHelper(fn, reporter, scope, structs) {
|
|
|
825
1053
|
reporter.fail(p.type, "helpers take accounts that exist: create Init/InitIfNeeded/Zeroed accounts in the instruction, then pass them as Mut<T>");
|
|
826
1054
|
return { name: p.name.text, type: { kind: "account" }, account, node: p };
|
|
827
1055
|
});
|
|
828
|
-
return { name
|
|
1056
|
+
return { name, display, params, returns, node: fn, statements: bodyStatements(fn, !returns), imports: scope.imports };
|
|
1057
|
+
}
|
|
1058
|
+
/** A function's statements; an arrow's expression body is a `return expr;` (or `expr;` when it returns void). */
|
|
1059
|
+
function bodyStatements(fn, isVoid) {
|
|
1060
|
+
const body = fn.body;
|
|
1061
|
+
if (ts.isBlock(body))
|
|
1062
|
+
return body.statements;
|
|
1063
|
+
const f = ts.factory;
|
|
1064
|
+
const stmt = isVoid ? f.createExpressionStatement(body) : f.createReturnStatement(body);
|
|
1065
|
+
ts.setTextRange(stmt, body);
|
|
1066
|
+
stmt.parent = fn;
|
|
1067
|
+
return [stmt];
|
|
1068
|
+
}
|
|
1069
|
+
/** The helper (or generic) a call's callee names: a local function by its declaration, others by name. */
|
|
1070
|
+
export function calleeName(callee, decls, checker) {
|
|
1071
|
+
let sym = checker.getSymbolAtLocation(callee);
|
|
1072
|
+
if (sym && sym.flags & ts.SymbolFlags.Alias)
|
|
1073
|
+
sym = checker.getAliasedSymbol(sym);
|
|
1074
|
+
for (const d of sym?.declarations ?? []) {
|
|
1075
|
+
const name = decls.get(d);
|
|
1076
|
+
if (name)
|
|
1077
|
+
return name;
|
|
1078
|
+
}
|
|
1079
|
+
return undefined;
|
|
829
1080
|
}
|
|
830
1081
|
/** Helpers may call each other, but not recursively: SBF has a small fixed call depth. */
|
|
831
|
-
function checkRecursion(
|
|
1082
|
+
function checkRecursion(sources, decls, checker, reporter) {
|
|
832
1083
|
const calls = new Map();
|
|
833
|
-
|
|
1084
|
+
const byName = new Map(sources.map((s) => [s.name, s]));
|
|
1085
|
+
for (const h of sources) {
|
|
834
1086
|
const out = [];
|
|
835
1087
|
const visit = (n) => {
|
|
836
|
-
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)
|
|
837
|
-
|
|
1088
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
|
|
1089
|
+
const c = calleeName(n.expression, decls, checker);
|
|
1090
|
+
if (c)
|
|
1091
|
+
out.push(c);
|
|
1092
|
+
}
|
|
838
1093
|
ts.forEachChild(n, visit);
|
|
839
1094
|
};
|
|
840
1095
|
visit(h.node.body);
|
|
@@ -844,14 +1099,15 @@ function checkRecursion(helpers, reporter) {
|
|
|
844
1099
|
const walk = (name, path) => {
|
|
845
1100
|
if (state.get(name) === "done")
|
|
846
1101
|
return;
|
|
1102
|
+
const src = byName.get(name);
|
|
847
1103
|
if (state.get(name) === "visiting")
|
|
848
|
-
reporter.fail(
|
|
1104
|
+
reporter.fail(src.node.name ?? src.node, `recursion is not supported: ${[...path, name].map((n) => byName.get(n).display).join(" -> ")}`);
|
|
849
1105
|
state.set(name, "visiting");
|
|
850
1106
|
for (const c of calls.get(name) ?? [])
|
|
851
1107
|
walk(c, [...path, name]);
|
|
852
1108
|
state.set(name, "done");
|
|
853
1109
|
};
|
|
854
|
-
for (const name of
|
|
1110
|
+
for (const name of byName.keys())
|
|
855
1111
|
walk(name, []);
|
|
856
1112
|
}
|
|
857
1113
|
function analyzeInstruction(fn, reporter, scope, structs) {
|
|
@@ -865,8 +1121,19 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
865
1121
|
let returns;
|
|
866
1122
|
if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword) {
|
|
867
1123
|
returns = resolveScalarType(fn.type, scope);
|
|
1124
|
+
const s = !returns && ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments
|
|
1125
|
+
? structs.get(fn.type.typeName.text)
|
|
1126
|
+
: undefined;
|
|
1127
|
+
if (s) {
|
|
1128
|
+
if (s.size === undefined || s.vec)
|
|
1129
|
+
reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be return data`);
|
|
1130
|
+
// The runtime caps return data at 1024 bytes.
|
|
1131
|
+
if (s.size > MAX_RETURN_DATA)
|
|
1132
|
+
reporter.fail(fn.type, `${s.name} is ${s.size} bytes; return data holds at most ${MAX_RETURN_DATA}`);
|
|
1133
|
+
returns = { kind: "struct", name: s.name };
|
|
1134
|
+
}
|
|
868
1135
|
if (!returns)
|
|
869
|
-
reporter.fail(fn.type, "instructions return void
|
|
1136
|
+
reporter.fail(fn.type, "instructions return void, one integer, boolean or Pubkey, or a fixed-size struct (sent as return data)");
|
|
870
1137
|
}
|
|
871
1138
|
const accounts = [];
|
|
872
1139
|
const args = [];
|
|
@@ -996,7 +1263,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
996
1263
|
const pushes = (() => {
|
|
997
1264
|
let found = false;
|
|
998
1265
|
const visit = (n) => {
|
|
999
|
-
|
|
1266
|
+
// Vec.push(value, payer) grows the account; FixedVec.push(value) does not.
|
|
1267
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "push" && n.arguments.length === 2)
|
|
1000
1268
|
found = true;
|
|
1001
1269
|
ts.forEachChild(n, visit);
|
|
1002
1270
|
};
|
|
@@ -1193,6 +1461,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1193
1461
|
reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
|
|
1194
1462
|
if (s.vec)
|
|
1195
1463
|
reporter.fail(node, `${s.name} ends in a Vec; Zeroed<T> is for fixed-size (large) accounts`);
|
|
1464
|
+
if (s.layout === "borsh")
|
|
1465
|
+
reporter.fail(node, `${s.name} is @layout borsh; Zeroed<T> reads large accounts in place, so it takes fixed layouts only`);
|
|
1196
1466
|
return { kind: "zeroed", struct: s };
|
|
1197
1467
|
}
|
|
1198
1468
|
case "Mut": {
|
|
@@ -30,10 +30,17 @@ export declare abstract class EmitterBase {
|
|
|
30
30
|
protected readonly helper: Helper | undefined;
|
|
31
31
|
protected readonly scope: TypeScope;
|
|
32
32
|
protected readonly checker: ts.TypeChecker;
|
|
33
|
+
protected readonly analysis: Analysis;
|
|
34
|
+
/** Calls already resolved to a helper (a generic one's instance is found by probing its arguments). */
|
|
35
|
+
private readonly calleeCache;
|
|
33
36
|
/** `file.ts:line` of the statement being lowered. */
|
|
34
37
|
protected where: string | undefined;
|
|
35
38
|
protected readonly profile: boolean;
|
|
36
39
|
constructor(analysis: Analysis, target: Instruction | Helper, opts?: EmitOptions);
|
|
40
|
+
/** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
|
|
41
|
+
protected helperOf(node: ts.CallExpression): Helper | undefined;
|
|
42
|
+
/** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
|
|
43
|
+
private typeArgsOf;
|
|
37
44
|
protected line(s: string): void;
|
|
38
45
|
/** `file.ts:line` of a node, for messages. */
|
|
39
46
|
protected location(node: ts.Node): string;
|