@typesol/cli 0.16.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 +46 -0
- package/README.md +3 -3
- package/dist/clientgen.js +37 -0
- package/dist/compiler/analyze.d.ts +7 -1
- package/dist/compiler/analyze.js +258 -30
- package/dist/compiler/emit/base.d.ts +7 -0
- package/dist/compiler/emit/base.js +61 -1
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +232 -0
- package/dist/compiler/emit/calls.js +43 -15
- package/dist/compiler/emit/cpi.d.ts +32 -8
- package/dist/compiler/emit/cpi.js +522 -20
- package/dist/compiler/emit/expressions.js +19 -2
- package/dist/compiler/emit/function.js +34 -11
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +12 -0
- package/dist/compiler/emit/shared.js +19 -2
- package/dist/compiler/emit/statements.js +11 -3
- package/dist/compiler/emit/storage.d.ts +4 -0
- package/dist/compiler/emit/storage.js +60 -12
- package/dist/compiler/idl.js +33 -4
- package/dist/compiler/model.d.ts +41 -2
- package/dist/compiler/model.js +8 -2
- package/dist/portgen.js +60 -4
- package/docs/errors.md +3 -2
- package/docs/language.md +10 -6
- package/docs/migrate-from-anchor.md +14 -5
- package/docs/test-your-program.md +1 -1
- package/lib/typesol.d.ts +259 -3
- package/package.json +1 -1
- package/runtime/typesol_rt.h +32 -7
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"]);
|
|
@@ -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",
|
|
@@ -225,7 +227,7 @@ export function analyze(entry) {
|
|
|
225
227
|
const isExported = (n) => ts.canHaveModifiers(n) && (ts.getModifiers(n) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
226
228
|
const declare = (name, file) => {
|
|
227
229
|
// These prefixes name the IDL types TypeSOL generates for FixedString<N>, Option and data enums in account data.
|
|
228
|
-
if (/^(FixedString\d|FixedOption_|FixedEnum_)/.test(name.text))
|
|
230
|
+
if (/^(FixedString\d|FixedVec\d|FixedOption_|FixedEnum_)/.test(name.text))
|
|
229
231
|
reporter.fail(name, `"${name.text}" is reserved for IDL types TypeSOL generates; pick another name`);
|
|
230
232
|
const prev = declared.get(name.text);
|
|
231
233
|
if (prev)
|
|
@@ -313,7 +315,8 @@ export function analyze(entry) {
|
|
|
313
315
|
else {
|
|
314
316
|
if (stmt.name)
|
|
315
317
|
declare(stmt.name, file);
|
|
316
|
-
|
|
318
|
+
if (stmt.name)
|
|
319
|
+
helperNodes.push({ node: stmt, name: stmt.name.text, display: stmt.name.text, decl: stmt, imports });
|
|
317
320
|
}
|
|
318
321
|
}
|
|
319
322
|
else if (ts.isVariableStatement(stmt)) {
|
|
@@ -324,6 +327,14 @@ export function analyze(entry) {
|
|
|
324
327
|
if (!ts.isIdentifier(d.name) || !d.initializer)
|
|
325
328
|
reporter.fail(d, "declare const NAME = literal");
|
|
326
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
|
+
}
|
|
327
338
|
let value;
|
|
328
339
|
if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && imports.get(init.expression.text) === "pubkey") {
|
|
329
340
|
const arg = init.arguments[0];
|
|
@@ -358,6 +369,7 @@ export function analyze(entry) {
|
|
|
358
369
|
}
|
|
359
370
|
}
|
|
360
371
|
const fstrings = new Set();
|
|
372
|
+
const fixedVecs = new Map();
|
|
361
373
|
const scopeFor = (imports) => ({ imports, enums, options, dataEnums });
|
|
362
374
|
for (const c of pendingConstTypes) {
|
|
363
375
|
reporter.guard(() => {
|
|
@@ -372,14 +384,87 @@ export function analyze(entry) {
|
|
|
372
384
|
c.def.type = t;
|
|
373
385
|
});
|
|
374
386
|
}
|
|
375
|
-
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));
|
|
376
429
|
const helpers = new Map();
|
|
377
|
-
|
|
378
|
-
|
|
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));
|
|
379
450
|
if (h)
|
|
380
451
|
helpers.set(h.name, h);
|
|
381
452
|
}
|
|
382
|
-
|
|
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));
|
|
383
468
|
const accountStructs = new Set();
|
|
384
469
|
const argStructs = new Set();
|
|
385
470
|
const instructions = [];
|
|
@@ -390,6 +475,23 @@ export function analyze(entry) {
|
|
|
390
475
|
instructions.push(ix);
|
|
391
476
|
for (const a of ix.args)
|
|
392
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
|
+
}
|
|
393
495
|
// A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
|
|
394
496
|
if (ix.returns?.kind === "struct")
|
|
395
497
|
collectStructs(ix.returns, structs, dataEnums, argStructs);
|
|
@@ -426,6 +528,8 @@ export function analyze(entry) {
|
|
|
426
528
|
structs,
|
|
427
529
|
instructions,
|
|
428
530
|
helpers,
|
|
531
|
+
generics,
|
|
532
|
+
helperDecls,
|
|
429
533
|
errors,
|
|
430
534
|
errorEnums,
|
|
431
535
|
enums,
|
|
@@ -436,12 +540,14 @@ export function analyze(entry) {
|
|
|
436
540
|
accountStructs,
|
|
437
541
|
argStructs,
|
|
438
542
|
fstrings,
|
|
543
|
+
fixedVecs,
|
|
439
544
|
},
|
|
440
545
|
sourceFile,
|
|
441
546
|
files: programFiles(program, entryPath).map((f) => f.fileName),
|
|
442
547
|
imports: entryImports,
|
|
443
548
|
reporter,
|
|
444
|
-
checker
|
|
549
|
+
checker,
|
|
550
|
+
instantiate,
|
|
445
551
|
};
|
|
446
552
|
}
|
|
447
553
|
/** Every struct an argument type contains (transitively), for IDL layout decisions. */
|
|
@@ -515,6 +621,11 @@ export function typesolName(node, imports) {
|
|
|
515
621
|
export function resolveScalarType(node, scope) {
|
|
516
622
|
if (node.kind === ts.SyntaxKind.BooleanKeyword)
|
|
517
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
|
+
}
|
|
518
629
|
const n = typesolName(node, scope.imports);
|
|
519
630
|
if (n && INT_NAMES.includes(n))
|
|
520
631
|
return { kind: "int", name: n };
|
|
@@ -572,7 +683,7 @@ function numericLiteralArg(node) {
|
|
|
572
683
|
* Resolve every exported interface. Field types may name other interfaces, so names are
|
|
573
684
|
* collected first; sizes are computed after, rejecting cycles.
|
|
574
685
|
*/
|
|
575
|
-
function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings) {
|
|
686
|
+
function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings, fixedVecs) {
|
|
576
687
|
const names = new Set(items.map((d) => d.decl.name.text));
|
|
577
688
|
const enumNames = new Set(aliases.map((a) => a.decl.name.text));
|
|
578
689
|
const structs = new Map();
|
|
@@ -628,6 +739,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
628
739
|
reporter.fail(targs[1], `array length must be 1..${MAX_ARRAY_LEN}`);
|
|
629
740
|
return { kind: "array", elem, len };
|
|
630
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
|
+
}
|
|
631
755
|
if (ext === "FixedString") {
|
|
632
756
|
const cap = numericLiteralArg(targs[0]);
|
|
633
757
|
if (cap === undefined || !Number.isInteger(cap) || cap < 1 || cap > MAX_STRING_CAP)
|
|
@@ -637,7 +761,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
637
761
|
}
|
|
638
762
|
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && names.has(node.typeName.text) && !targs.length)
|
|
639
763
|
return { kind: "struct", name: node.typeName.text };
|
|
640
|
-
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");
|
|
641
765
|
};
|
|
642
766
|
for (const { decl, imports: fileImports } of items) {
|
|
643
767
|
scope = scopeFor(fileImports);
|
|
@@ -658,12 +782,17 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
658
782
|
if (problem)
|
|
659
783
|
reporter.fail(m.name, problem);
|
|
660
784
|
const type = fieldType(m.type);
|
|
661
|
-
if (type.kind === "vec" && m !== decl.members[decl.members.length - 1])
|
|
662
|
-
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");
|
|
663
787
|
fields.push({ name, type });
|
|
664
788
|
}
|
|
665
789
|
checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
|
|
666
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)");
|
|
667
796
|
structs.set(name, {
|
|
668
797
|
name,
|
|
669
798
|
fields,
|
|
@@ -671,6 +800,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
671
800
|
accountDiscriminator: discriminator(`account:${name}`),
|
|
672
801
|
eventDiscriminator: discriminator(`event:${name}`),
|
|
673
802
|
node: decl,
|
|
803
|
+
layout: layoutText === "borsh" ? "borsh" : "fixed",
|
|
674
804
|
});
|
|
675
805
|
});
|
|
676
806
|
}
|
|
@@ -766,8 +896,14 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
766
896
|
return undefined;
|
|
767
897
|
return 1 + e.payload;
|
|
768
898
|
}
|
|
769
|
-
case "vec":
|
|
770
|
-
|
|
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
|
+
}
|
|
771
907
|
case "string":
|
|
772
908
|
return undefined;
|
|
773
909
|
case "fstring":
|
|
@@ -806,7 +942,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
806
942
|
if (total !== undefined && total + 8 > MAX_ACCOUNT_SIZE)
|
|
807
943
|
reporter.fail(s.node.name, `${s.name} is ${total + 8} bytes with its discriminator; Solana accounts are at most ${MAX_ACCOUNT_SIZE} bytes`);
|
|
808
944
|
const last = s.fields.at(-1);
|
|
809
|
-
if (last?.type.kind === "vec") {
|
|
945
|
+
if (last?.type.kind === "vec" && last.type.cap === undefined) {
|
|
810
946
|
const elemSize = sizeOf(last.type.elem, s.node);
|
|
811
947
|
if (elemSize === undefined)
|
|
812
948
|
reporter.fail(s.node, "Vec elements must have a fixed size");
|
|
@@ -816,24 +952,85 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings)
|
|
|
816
952
|
};
|
|
817
953
|
for (const s of structs.values())
|
|
818
954
|
reporter.guard(() => visit(s));
|
|
955
|
+
for (const s of structs.values())
|
|
956
|
+
if (s.layout === "borsh")
|
|
957
|
+
reporter.guard(() => checkBorshLayout(s, structs, reporter));
|
|
819
958
|
for (const e of dataEnums.values())
|
|
820
959
|
reporter.guard(() => sizeOf({ kind: "dataEnum", name: e.name }, e.node));
|
|
821
960
|
return structs;
|
|
822
961
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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");
|
|
828
1025
|
if (!fn.type)
|
|
829
|
-
reporter.fail(fn.name, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
|
|
1026
|
+
reporter.fail(fn.name ?? fn, "helper functions need an explicit return type (void, an integer, boolean, Pubkey or a struct)");
|
|
830
1027
|
let returns = fn.type.kind === ts.SyntaxKind.VoidKeyword ? undefined : resolveScalarType(fn.type, scope);
|
|
831
1028
|
if (fn.type.kind !== ts.SyntaxKind.VoidKeyword && !returns) {
|
|
832
1029
|
const s = ts.isTypeReferenceNode(fn.type) && ts.isIdentifier(fn.type.typeName) && !fn.type.typeArguments ? structs.get(fn.type.typeName.text) : undefined;
|
|
833
1030
|
if (s && (s.size === undefined || s.vec))
|
|
834
1031
|
reporter.fail(fn.type, `${s.name} has variable-size fields and cannot be returned; return a struct of fixed size`);
|
|
835
1032
|
if (!s)
|
|
836
|
-
reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey or a
|
|
1033
|
+
reporter.fail(fn.type, "helpers return void, an integer, boolean, Pubkey, a struct or a type parameter");
|
|
837
1034
|
returns = { kind: "struct", name: s.name };
|
|
838
1035
|
}
|
|
839
1036
|
const params = fn.parameters.map((p) => {
|
|
@@ -856,16 +1053,43 @@ function analyzeHelper(fn, reporter, scope, structs) {
|
|
|
856
1053
|
reporter.fail(p.type, "helpers take accounts that exist: create Init/InitIfNeeded/Zeroed accounts in the instruction, then pass them as Mut<T>");
|
|
857
1054
|
return { name: p.name.text, type: { kind: "account" }, account, node: p };
|
|
858
1055
|
});
|
|
859
|
-
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;
|
|
860
1080
|
}
|
|
861
1081
|
/** Helpers may call each other, but not recursively: SBF has a small fixed call depth. */
|
|
862
|
-
function checkRecursion(
|
|
1082
|
+
function checkRecursion(sources, decls, checker, reporter) {
|
|
863
1083
|
const calls = new Map();
|
|
864
|
-
|
|
1084
|
+
const byName = new Map(sources.map((s) => [s.name, s]));
|
|
1085
|
+
for (const h of sources) {
|
|
865
1086
|
const out = [];
|
|
866
1087
|
const visit = (n) => {
|
|
867
|
-
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)
|
|
868
|
-
|
|
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
|
+
}
|
|
869
1093
|
ts.forEachChild(n, visit);
|
|
870
1094
|
};
|
|
871
1095
|
visit(h.node.body);
|
|
@@ -875,14 +1099,15 @@ function checkRecursion(helpers, reporter) {
|
|
|
875
1099
|
const walk = (name, path) => {
|
|
876
1100
|
if (state.get(name) === "done")
|
|
877
1101
|
return;
|
|
1102
|
+
const src = byName.get(name);
|
|
878
1103
|
if (state.get(name) === "visiting")
|
|
879
|
-
reporter.fail(
|
|
1104
|
+
reporter.fail(src.node.name ?? src.node, `recursion is not supported: ${[...path, name].map((n) => byName.get(n).display).join(" -> ")}`);
|
|
880
1105
|
state.set(name, "visiting");
|
|
881
1106
|
for (const c of calls.get(name) ?? [])
|
|
882
1107
|
walk(c, [...path, name]);
|
|
883
1108
|
state.set(name, "done");
|
|
884
1109
|
};
|
|
885
|
-
for (const name of
|
|
1110
|
+
for (const name of byName.keys())
|
|
886
1111
|
walk(name, []);
|
|
887
1112
|
}
|
|
888
1113
|
function analyzeInstruction(fn, reporter, scope, structs) {
|
|
@@ -1038,7 +1263,8 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1038
1263
|
const pushes = (() => {
|
|
1039
1264
|
let found = false;
|
|
1040
1265
|
const visit = (n) => {
|
|
1041
|
-
|
|
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)
|
|
1042
1268
|
found = true;
|
|
1043
1269
|
ts.forEachChild(n, visit);
|
|
1044
1270
|
};
|
|
@@ -1235,6 +1461,8 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1235
1461
|
reporter.fail(node, "Zeroed<T> needs a struct declared in this program");
|
|
1236
1462
|
if (s.vec)
|
|
1237
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`);
|
|
1238
1466
|
return { kind: "zeroed", struct: s };
|
|
1239
1467
|
}
|
|
1240
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;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
+
import { calleeName, resolveScalarType } from "../analyze.js";
|
|
3
|
+
import { isScalar, } from "../model.js";
|
|
2
4
|
import { isStable, cString, valueC } from "./shared.js";
|
|
3
5
|
/**
|
|
4
6
|
* State of one function being lowered, and the primitives every layer uses: emitting lines, temporaries,
|
|
@@ -27,6 +29,9 @@ export class EmitterBase {
|
|
|
27
29
|
helper;
|
|
28
30
|
scope;
|
|
29
31
|
checker;
|
|
32
|
+
analysis;
|
|
33
|
+
/** Calls already resolved to a helper (a generic one's instance is found by probing its arguments). */
|
|
34
|
+
calleeCache = new Map();
|
|
30
35
|
/** `file.ts:line` of the statement being lowered. */
|
|
31
36
|
where;
|
|
32
37
|
profile;
|
|
@@ -36,8 +41,15 @@ export class EmitterBase {
|
|
|
36
41
|
this.model = analysis.model;
|
|
37
42
|
// Each function resolves `typesol` names through its own file's imports.
|
|
38
43
|
this.imports = target.imports;
|
|
39
|
-
this.scope = {
|
|
44
|
+
this.scope = {
|
|
45
|
+
imports: target.imports,
|
|
46
|
+
enums: analysis.model.enums,
|
|
47
|
+
options: analysis.model.options,
|
|
48
|
+
dataEnums: analysis.model.dataEnums,
|
|
49
|
+
typeArgs: "accounts" in target ? undefined : target.typeArgs,
|
|
50
|
+
};
|
|
40
51
|
this.checker = analysis.checker;
|
|
52
|
+
this.analysis = analysis;
|
|
41
53
|
if ("accounts" in target) {
|
|
42
54
|
this.ix = target;
|
|
43
55
|
for (const a of target.accounts) {
|
|
@@ -66,6 +78,54 @@ export class EmitterBase {
|
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
}
|
|
81
|
+
/** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
|
|
82
|
+
helperOf(node) {
|
|
83
|
+
if (!ts.isIdentifier(node.expression))
|
|
84
|
+
return undefined;
|
|
85
|
+
const cached = this.calleeCache.get(node);
|
|
86
|
+
if (cached !== undefined)
|
|
87
|
+
return cached ?? undefined;
|
|
88
|
+
const callee = node.expression;
|
|
89
|
+
const name = calleeName(callee, this.model.helperDecls, this.checker) ?? (this.lookup(callee.text) ? undefined : callee.text);
|
|
90
|
+
const g = name ? this.model.generics.get(name) : undefined;
|
|
91
|
+
let h = name ? this.model.helpers.get(name) : undefined;
|
|
92
|
+
if (g)
|
|
93
|
+
h = this.analysis.instantiate(g, this.typeArgsOf(node, g));
|
|
94
|
+
else if (h && node.typeArguments)
|
|
95
|
+
this.fail(node, `${h.display} takes no type arguments`);
|
|
96
|
+
this.calleeCache.set(node, h ?? null);
|
|
97
|
+
return h;
|
|
98
|
+
}
|
|
99
|
+
/** A generic call's type arguments: written out (`max<u64>(a, b)`), or the types of the arguments in `T` positions. */
|
|
100
|
+
typeArgsOf(node, g) {
|
|
101
|
+
const usage = `write ${g.display}<u64>(…) with the types`;
|
|
102
|
+
if (node.typeArguments) {
|
|
103
|
+
if (node.typeArguments.length !== g.typeParams.length)
|
|
104
|
+
this.fail(node, `${g.display} takes ${g.typeParams.length} type argument(s)`);
|
|
105
|
+
return node.typeArguments.map((t) => resolveScalarType(t, this.scope) ?? this.fail(t, "type arguments are integers, boolean, Pubkey or enums"));
|
|
106
|
+
}
|
|
107
|
+
return g.typeParams.map((tp) => {
|
|
108
|
+
for (const [i, p] of g.node.parameters.entries()) {
|
|
109
|
+
const arg = node.arguments[i];
|
|
110
|
+
if (!arg || !p.type || !ts.isTypeReferenceNode(p.type) || !ts.isIdentifier(p.type.typeName) || p.type.typeName.text !== tp || p.type.typeArguments)
|
|
111
|
+
continue;
|
|
112
|
+
// Type the argument without emitting it: it is lowered again, once, by the call.
|
|
113
|
+
const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
114
|
+
const probe = this.reporter.guard(() => this.expr(arg));
|
|
115
|
+
this.lines.length = saved.lines;
|
|
116
|
+
this.hoisted.length = saved.hoisted;
|
|
117
|
+
this.temp = saved.temp;
|
|
118
|
+
this.reporter.diagnostics.length = saved.diagnostics;
|
|
119
|
+
if (!probe || probe.literal !== undefined || probe.isNull)
|
|
120
|
+
continue;
|
|
121
|
+
if (!isScalar(probe.type))
|
|
122
|
+
this.fail(arg, `${tp} stands for an integer, boolean, Pubkey or enum, not ${probe.type.kind}`);
|
|
123
|
+
const t = probe.type;
|
|
124
|
+
return t.kind === "pubkey" ? (t.hash ? { kind: "pubkey", hash: true } : { kind: "pubkey" }) : t;
|
|
125
|
+
}
|
|
126
|
+
return this.fail(node, `cannot tell what ${tp} is from literal arguments; ${usage}`);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
69
129
|
line(s) {
|
|
70
130
|
// Runtime failures name the TypeScript line they come from: "TypeSOL: arithmetic overflow (vault.ts:42)".
|
|
71
131
|
if (this.where)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FieldType, ProgramModel, StructDef } from "../model.js";
|
|
2
|
+
/**
|
|
3
|
+
* Accounts declared `@layout borsh` are stored as Anchor stores them (Borsh: a String or Vec takes 4 + its length, an
|
|
4
|
+
* empty Option one byte, and the fields after them move). An instruction unpacks such an account into the usual
|
|
5
|
+
* fixed-layout struct on the heap (`ts_unpack_<S>`, checking it as Borsh deserialization would), runs on that, and
|
|
6
|
+
* packs it back when it returns (`ts_pack_<S>`), so every language feature works on it unchanged.
|
|
7
|
+
*/
|
|
8
|
+
/** A field type whose Borsh bytes are its fixed-layout bytes: scalars, Pubkeys, unit enums, arrays and structs of them. */
|
|
9
|
+
export declare function borshIdentical(t: FieldType, model: Pick<ProgramModel, "structs">): boolean;
|
|
10
|
+
/** The fewest bytes a Borsh `S` can take (every String and Vec empty, every Option None). */
|
|
11
|
+
export declare function borshMinSize(s: StructDef, model: Pick<ProgramModel, "structs">): number;
|
|
12
|
+
/** C for `ts_unpack_<S>` and `ts_pack_<S>` of every `@layout borsh` struct. */
|
|
13
|
+
export declare function emitBorshCodecs(model: ProgramModel): string[];
|