@typesol/cli 0.22.0 → 0.23.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 +21 -0
- package/README.md +1 -1
- package/dist/compiler/analyze.js +93 -15
- package/dist/compiler/emit/base.d.ts +11 -1
- package/dist/compiler/emit/base.js +21 -0
- package/dist/compiler/emit/calls.d.ts +11 -1
- package/dist/compiler/emit/calls.js +68 -6
- package/dist/compiler/emit/cpi.js +42 -4
- package/dist/compiler/emit/expressions.js +9 -0
- package/dist/compiler/emit/function.js +5 -3
- package/dist/compiler/emit/program.js +9 -2
- package/dist/compiler/emit/shared.d.ts +32 -2
- package/dist/compiler/emit/shared.js +43 -0
- package/dist/compiler/emit/statements.d.ts +35 -0
- package/dist/compiler/emit/statements.js +237 -79
- package/dist/compiler/emit/storage.js +39 -13
- package/dist/compiler/model.d.ts +18 -1
- package/docs/language.md +9 -9
- package/lib/typesol.d.ts +8 -0
- package/package.json +1 -1
- package/runtime/typesol_rt.h +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ until 1.0, a minor version may change the language, the IDL layout or error code
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.23.0 — 2026-10-28
|
|
9
|
+
|
|
10
|
+
Language limits lifted: `map` and `filter`, any number of growable Vecs per account, closures and arrow functions as
|
|
11
|
+
helper arguments, FixedVec arguments and list fields in events. Existing programs compile to the same bytes. Not
|
|
12
|
+
externally audited (see SECURITY.md).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `map` and `filter` on Vecs, FixedArrays, FixedVecs and Vec arguments. A chain used right away
|
|
16
|
+
(`xs.filter(f).map(g).reduce(h, 0n)`, `.length`, `for … of`, any list method) is one loop that builds no list;
|
|
17
|
+
callbacks after a filter get the index among the elements it let through, as in JS. Stored (`const ys = xs.map(f)`),
|
|
18
|
+
it is a FixedVec on the stack as big as the list it starts from, or as annotated (`const ys: FixedVec<u64, 16> = …`,
|
|
19
|
+
needed for a `Vec`); a list one element too small aborts with 5104.
|
|
20
|
+
- Arrow functions written as helper arguments (`applyTwice((x) => x * 3n, v)`), typed from the helper's parameter when
|
|
21
|
+
left unannotated. Given a closure (one that reads the caller's variables), the helper is expanded where it is called,
|
|
22
|
+
so the closure runs where it can.
|
|
23
|
+
- Any number of growable `Vec<T>` fields in an account, declared anywhere. They are stored after the fixed-size fields,
|
|
24
|
+
in the order declared (every other field keeps a fixed offset; the IDL lists the stored order), and a push, pop or
|
|
25
|
+
clear moves the Vecs after it. An account with one Vec is laid out as before.
|
|
26
|
+
- `FixedVec<T, N>` arguments (a Borsh Vec of at most N elements; more fail with 102), and `FixedVec` and `Vec` fields
|
|
27
|
+
in events.
|
|
28
|
+
|
|
8
29
|
## 0.22.0 — 2026-10-28
|
|
9
30
|
|
|
10
31
|
Generated clients fill in the accounts Anchor's client still asked you to pass (signers from a default signer, PDAs
|
package/README.md
CHANGED
|
@@ -306,7 +306,7 @@ Details are in [docs/testing.md](docs/testing.md).
|
|
|
306
306
|
|
|
307
307
|
## Status
|
|
308
308
|
|
|
309
|
-
**TypeSOL is pre-1.0 (0.
|
|
309
|
+
**TypeSOL is pre-1.0 (0.23).** Releases follow the organization's
|
|
310
310
|
[versioning policy](https://github.com/typesol/.github/blob/main/VERSIONING.md); [CHANGELOG.md](CHANGELOG.md) lists every
|
|
311
311
|
change.
|
|
312
312
|
- The compiler, runtime and tooling have been through internal security reviews and are covered by the tests above.
|
package/dist/compiler/analyze.js
CHANGED
|
@@ -422,6 +422,32 @@ 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 target = (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && ts.isCallExpression(n.parent) ? fnParamType(n, checker) : undefined;
|
|
428
|
+
if (target) {
|
|
429
|
+
reporter.guard(() => {
|
|
430
|
+
if (generic)
|
|
431
|
+
reporter.fail(n, "arrow functions passed inside a generic helper are not supported; declare the function at the top level");
|
|
432
|
+
const lit = n;
|
|
433
|
+
lit.parameters.forEach((p, i) => {
|
|
434
|
+
const from = target.parameters[i]?.type;
|
|
435
|
+
if (!p.type && from)
|
|
436
|
+
p.type = from;
|
|
437
|
+
});
|
|
438
|
+
if (!lit.type)
|
|
439
|
+
lit.type = target.type;
|
|
440
|
+
let name = `${outer}__fn`;
|
|
441
|
+
for (let i = 2; taken.has(name); i++)
|
|
442
|
+
name = `${outer}__fn${i}`;
|
|
443
|
+
taken.add(name);
|
|
444
|
+
const src = { node: lit, name, display: "the function passed here", decl: lit, imports, outer: fn };
|
|
445
|
+
locals.push(src);
|
|
446
|
+
if (lit.body)
|
|
447
|
+
collectLocals(lit.body, name, imports, lit);
|
|
448
|
+
});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
425
451
|
ts.forEachChild(n, visit);
|
|
426
452
|
};
|
|
427
453
|
visit(body);
|
|
@@ -525,7 +551,7 @@ export function analyze(entry) {
|
|
|
525
551
|
collectStructs(a.type, structs, dataEnums, seen);
|
|
526
552
|
for (const s of seen)
|
|
527
553
|
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
|
|
554
|
+
reporter.error(fn.node.name ?? fn.node, `argument ${a.name}: ${s} holds a FixedVec, which only account data can; make ${s} @layout borsh, or pass the list as its own FixedVec<T, N> argument`);
|
|
529
555
|
}
|
|
530
556
|
// A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
|
|
531
557
|
if (ix.returns?.kind === "struct")
|
|
@@ -580,6 +606,7 @@ export function analyze(entry) {
|
|
|
580
606
|
argStructs,
|
|
581
607
|
fstrings,
|
|
582
608
|
fixedVecs,
|
|
609
|
+
localVecs: new Map(),
|
|
583
610
|
},
|
|
584
611
|
sourceFile,
|
|
585
612
|
files: programFiles(program, entryPath).map((f) => f.fileName),
|
|
@@ -642,11 +669,15 @@ export function resolveArgAggregate(node, scope, structs) {
|
|
|
642
669
|
return { kind: "structRef", struct: top.name, writable: false };
|
|
643
670
|
if (top?.kind === "array")
|
|
644
671
|
return { kind: "arrayArg", array: top };
|
|
645
|
-
|
|
646
|
-
|
|
672
|
+
const list = typesolName(node, scope.imports);
|
|
673
|
+
if (list === "Vec" || list === "FixedVec") {
|
|
674
|
+
const [e, n] = node.typeArguments ?? [];
|
|
647
675
|
const elem = e && fixed(e);
|
|
676
|
+
const cap = list === "FixedVec" ? numericLiteralArg(n) : undefined;
|
|
677
|
+
if (list === "FixedVec" && !(cap !== undefined && Number.isInteger(cap) && cap >= 1))
|
|
678
|
+
return undefined;
|
|
648
679
|
if (elem && (elem.kind === "struct" || elem.kind === "array"))
|
|
649
|
-
return { kind: "vecArg", elem };
|
|
680
|
+
return { kind: "vecArg", elem, ...(cap ? { cap } : {}) };
|
|
650
681
|
}
|
|
651
682
|
return undefined;
|
|
652
683
|
}
|
|
@@ -714,6 +745,14 @@ export function resolveValueType(node, scope, allowVec = false) {
|
|
|
714
745
|
if (elem)
|
|
715
746
|
return { kind: "vecArg", elem };
|
|
716
747
|
}
|
|
748
|
+
// FixedVec<T, N> as an argument: a Borsh Vec of at most N elements (more fail to deserialize), read like a Vec argument.
|
|
749
|
+
if (allowVec && typesolName(node, scope.imports) === "FixedVec") {
|
|
750
|
+
const [elemNode, n] = node.typeArguments ?? [];
|
|
751
|
+
const elem = elemNode && resolveScalarType(elemNode, scope);
|
|
752
|
+
const cap = numericLiteralArg(n);
|
|
753
|
+
if (elem && cap !== undefined && Number.isInteger(cap) && cap >= 1)
|
|
754
|
+
return { kind: "vecArg", elem, cap };
|
|
755
|
+
}
|
|
717
756
|
return resolveScalarType(node, scope);
|
|
718
757
|
}
|
|
719
758
|
function numericLiteralArg(node) {
|
|
@@ -824,9 +863,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
|
|
|
824
863
|
if (problem)
|
|
825
864
|
reporter.fail(m.name, problem);
|
|
826
865
|
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 });
|
|
866
|
+
fields.push({ name, type, node: m });
|
|
830
867
|
}
|
|
831
868
|
checkSnakeDuplicates(decl.members.map((m) => ({ name: m.name.text, node: m })), "fields", reporter);
|
|
832
869
|
const name = decl.name.text;
|
|
@@ -835,9 +872,20 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
|
|
|
835
872
|
const layoutText = typeof layoutTag?.comment === "string" ? layoutTag.comment.trim() : undefined;
|
|
836
873
|
if (layoutTag && layoutText !== "borsh" && layoutText !== "fixed")
|
|
837
874
|
reporter.fail(layoutTag, "@layout is borsh (Anchor's account bytes) or fixed (the default)");
|
|
875
|
+
const growable = (f) => f.type.kind === "vec" && f.type.cap === undefined;
|
|
876
|
+
// Anchor's bytes keep the declared order, so there a growable Vec has to be last (the fields after it would move).
|
|
877
|
+
if (layoutText === "borsh")
|
|
878
|
+
for (const f of fields.slice(0, -1))
|
|
879
|
+
if (growable(f))
|
|
880
|
+
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");
|
|
881
|
+
// The fixed layout stores the growable Vecs after the fixed-size fields, in the order declared, so every other
|
|
882
|
+
// field keeps a fixed offset; the IDL lists the fields in this stored order.
|
|
883
|
+
const stored = layoutText === "borsh" ? fields : [...fields.filter((f) => !growable(f)), ...fields.filter(growable)];
|
|
884
|
+
for (const f of stored)
|
|
885
|
+
delete f.node;
|
|
838
886
|
structs.set(name, {
|
|
839
887
|
name,
|
|
840
|
-
fields,
|
|
888
|
+
fields: stored,
|
|
841
889
|
size: undefined,
|
|
842
890
|
accountDiscriminator: discriminator(`account:${name}`),
|
|
843
891
|
eventDiscriminator: discriminator(`event:${name}`),
|
|
@@ -987,13 +1035,19 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
|
|
|
987
1035
|
s.size = total;
|
|
988
1036
|
if (total !== undefined && total + 8 > MAX_ACCOUNT_SIZE)
|
|
989
1037
|
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
|
-
|
|
1038
|
+
// Growable Vecs (stored last, see above): the first one's length and elements end the C struct; the others follow
|
|
1039
|
+
// it in the account, each a u32 length and its elements.
|
|
1040
|
+
for (const f of s.fields)
|
|
1041
|
+
if (f.type.kind === "vec" && f.type.cap === undefined) {
|
|
1042
|
+
const elemSize = sizeOf(f.type.elem, s.node);
|
|
1043
|
+
if (elemSize === undefined)
|
|
1044
|
+
reporter.fail(s.node, "Vec elements must have a fixed size");
|
|
1045
|
+
(s.vecs ??= []).push({ name: f.name, elem: f.type.elem, elemSize });
|
|
1046
|
+
}
|
|
1047
|
+
s.vec = s.vecs?.[0];
|
|
1048
|
+
// The C struct holds the first Vec's length; each further one adds its own when the account is created.
|
|
1049
|
+
if (s.vecs && s.size !== undefined)
|
|
1050
|
+
s.size -= 4 * (s.vecs.length - 1);
|
|
997
1051
|
state.set(s.name, "done");
|
|
998
1052
|
};
|
|
999
1053
|
for (const s of structs.values())
|
|
@@ -1007,6 +1061,30 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
|
|
|
1007
1061
|
}
|
|
1008
1062
|
const isFunctionNode = (n) => ts.isFunctionDeclaration(n) || ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isMethodDeclaration(n);
|
|
1009
1063
|
/** Whether a local function reads a variable or parameter of a function around it (a closure). */
|
|
1064
|
+
/**
|
|
1065
|
+
* The function type of the helper parameter an arrow is passed for (`f` in `function apply(f: (x: u64) => u64, …)`),
|
|
1066
|
+
* when the arrow is an argument of a call to a declared function or const arrow; undefined otherwise.
|
|
1067
|
+
*/
|
|
1068
|
+
function fnParamType(arrow, checker) {
|
|
1069
|
+
const call = arrow.parent;
|
|
1070
|
+
const i = call.arguments.indexOf(arrow);
|
|
1071
|
+
if (i < 0 || !ts.isIdentifier(call.expression))
|
|
1072
|
+
return undefined;
|
|
1073
|
+
let sym = checker.getSymbolAtLocation(call.expression);
|
|
1074
|
+
if (sym && sym.flags & ts.SymbolFlags.Alias)
|
|
1075
|
+
sym = checker.getAliasedSymbol(sym);
|
|
1076
|
+
for (const d of sym?.declarations ?? []) {
|
|
1077
|
+
const fn = ts.isFunctionDeclaration(d)
|
|
1078
|
+
? d
|
|
1079
|
+
: ts.isVariableDeclaration(d) && d.initializer && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))
|
|
1080
|
+
? d.initializer
|
|
1081
|
+
: undefined;
|
|
1082
|
+
const t = fn?.parameters[i]?.type;
|
|
1083
|
+
if (t && ts.isFunctionTypeNode(t))
|
|
1084
|
+
return t;
|
|
1085
|
+
}
|
|
1086
|
+
return undefined;
|
|
1087
|
+
}
|
|
1010
1088
|
function captures(l, localDecls, checker) {
|
|
1011
1089
|
const fn = l.node;
|
|
1012
1090
|
let found = false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { type Analysis, 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,
|
|
@@ -50,6 +50,8 @@ export declare abstract class EmitterBase {
|
|
|
50
50
|
constructor(analysis: Analysis, target: Instruction | Helper, opts?: EmitOptions);
|
|
51
51
|
/** The helper a call runs: a function, a const arrow function (top-level or local), or a generic one's instance. */
|
|
52
52
|
protected helperOf(node: ts.CallExpression): Helper | undefined;
|
|
53
|
+
/** The helper a call runs, when it is given a closure for a function parameter (so it is expanded there). */
|
|
54
|
+
protected givenClosure(node: ts.CallExpression): GenericHelper | undefined;
|
|
53
55
|
/**
|
|
54
56
|
* The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
|
|
55
57
|
* captures nothing). A closure can only run where it is declared, so it cannot be passed.
|
|
@@ -116,4 +118,12 @@ export declare abstract class EmitterBase {
|
|
|
116
118
|
protected abstract epilogue(): string[];
|
|
117
119
|
protected abstract expandClosure(c: Closure, args: readonly ts.Expression[], node: ts.Node): Value | undefined;
|
|
118
120
|
protected abstract listMethod(node: ts.CallExpression, method: string): Value | undefined;
|
|
121
|
+
protected abstract isPipeline(e: ts.Expression): e is ts.CallExpression;
|
|
122
|
+
protected abstract closureOf(arg: ts.Expression): Closure;
|
|
123
|
+
protected abstract eachElement(list: ts.Expression, at: ts.Node, names: {
|
|
124
|
+
index: string;
|
|
125
|
+
ci: string;
|
|
126
|
+
next?: string;
|
|
127
|
+
alwaysNext?: boolean;
|
|
128
|
+
}, body: (element: ts.Expression, index: ts.Expression) => void): void;
|
|
119
129
|
}
|
|
@@ -87,6 +87,9 @@ export class EmitterBase {
|
|
|
87
87
|
helperOf(node) {
|
|
88
88
|
if (!ts.isIdentifier(node.expression))
|
|
89
89
|
return undefined;
|
|
90
|
+
// Given a closure, the helper is expanded where it is called (helperWithClosure), not called.
|
|
91
|
+
if (this.givenClosure(node))
|
|
92
|
+
return undefined;
|
|
90
93
|
const cached = this.calleeCache.get(node);
|
|
91
94
|
if (cached !== undefined)
|
|
92
95
|
return cached ?? undefined;
|
|
@@ -103,6 +106,17 @@ export class EmitterBase {
|
|
|
103
106
|
this.calleeCache.set(node, h ?? null);
|
|
104
107
|
return h;
|
|
105
108
|
}
|
|
109
|
+
/** The helper a call runs, when it is given a closure for a function parameter (so it is expanded there). */
|
|
110
|
+
givenClosure(node) {
|
|
111
|
+
if (!ts.isIdentifier(node.expression))
|
|
112
|
+
return undefined;
|
|
113
|
+
const name = calleeName(node.expression, this.model.helperDecls, this.checker) ?? (this.lookup(node.expression.text) ? undefined : node.expression.text);
|
|
114
|
+
const g = name ? this.model.generics.get(name) : undefined;
|
|
115
|
+
if (!g?.fnParams.length)
|
|
116
|
+
return undefined;
|
|
117
|
+
const closureArg = (a) => !!a && (ts.isArrowFunction(a) || ts.isFunctionExpression(a) ? !this.model.helperDecls.has(a) : ts.isIdentifier(a) && !!this.lookup(a.text)?.closure);
|
|
118
|
+
return g.fnParams.some((i) => closureArg(node.arguments[i])) ? g : undefined;
|
|
119
|
+
}
|
|
106
120
|
/**
|
|
107
121
|
* The functions passed for a helper's function parameters: named functions only (a helper, or a local function that
|
|
108
122
|
* captures nothing). A closure can only run where it is declared, so it cannot be passed.
|
|
@@ -111,6 +125,13 @@ export class EmitterBase {
|
|
|
111
125
|
return g.fnParams.map((i) => {
|
|
112
126
|
const arg = node.arguments[i];
|
|
113
127
|
const pname = g.node.parameters[i].name.text;
|
|
128
|
+
// An arrow written here: lifted to a local function when it reads nothing around it.
|
|
129
|
+
if (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))) {
|
|
130
|
+
const lifted = this.model.helperDecls.get(arg);
|
|
131
|
+
if (lifted)
|
|
132
|
+
return lifted;
|
|
133
|
+
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`);
|
|
134
|
+
}
|
|
114
135
|
if (!arg || !ts.isIdentifier(arg))
|
|
115
136
|
this.fail(arg ?? node, `pass ${g.display}'s ${pname} a named function (declare it: const f = (…) => …)`);
|
|
116
137
|
const passed = this.helper?.fnArgs?.get(arg.text);
|
|
@@ -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,16 @@ 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
|
+
/** A function given for a function parameter of an expanded helper or closure, as something to expand. */
|
|
43
|
+
protected closureOf(arg: ts.Expression): Closure;
|
|
34
44
|
protected callHelper(node: ts.CallExpression, h: Helper): Value | undefined;
|
|
35
45
|
/** One helper argument: a C argument list fragment (accounts, seeds, references) or a value to settle. */
|
|
36
46
|
protected helperArg(h: Helper, p: Helper["params"][number], n: ts.Expression): Value | string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { calleeName, resolveScalarType } from "../analyze.js";
|
|
2
|
+
import { calleeName, resolveScalarType, resolveValueType } from "../analyze.js";
|
|
3
3
|
import { ANCHOR_ERRORS } from "../anchor-errors.js";
|
|
4
4
|
import { decodeAddress } from "../../keys.js";
|
|
5
5
|
import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
|
|
6
|
-
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, } from "./shared.js";
|
|
6
|
+
import { U64, U8, I64, BOOL, PUBKEY, STRING, HASH, cBytes, cString, scalarC, scalarStorageC, optC, valueC, fieldC, storageDecl, accountMismatch, vecCount, emptySizeC, vecLenCheck, } from "./shared.js";
|
|
7
7
|
import { ExpressionEmitter } from "./expressions.js";
|
|
8
8
|
/**
|
|
9
9
|
* Calls: built-in functions, Vec methods, helpers, remaining accounts, foreign account reads and return data.
|
|
@@ -465,6 +465,9 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
465
465
|
if (local?.closure)
|
|
466
466
|
return this.expandClosure(local.closure, node.arguments, node);
|
|
467
467
|
}
|
|
468
|
+
const inlined = this.helperWithClosure(node);
|
|
469
|
+
if (inlined)
|
|
470
|
+
return inlined.value;
|
|
468
471
|
const helper = this.helperOf(node);
|
|
469
472
|
if (helper)
|
|
470
473
|
return this.callHelper(node, helper);
|
|
@@ -473,9 +476,12 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
473
476
|
// xs.some(…) and friends on a Vec, FixedArray, FixedVec or Vec argument: a loop with the callback expanded in it.
|
|
474
477
|
if (LIST_METHODS.has(method)) {
|
|
475
478
|
const l = ts.isIdentifier(callee.expression) ? this.lookup(callee.expression.text) : undefined;
|
|
476
|
-
if (l?.type.kind === "vecArg" || l?.type.kind === "array" || this.peekArray(callee.expression))
|
|
479
|
+
if (l?.type.kind === "vecArg" || l?.type.kind === "array" || this.isPipeline(callee.expression) || this.peekArray(callee.expression))
|
|
477
480
|
return this.listMethod(node, method);
|
|
478
481
|
}
|
|
482
|
+
// A map / filter chain is consumed by a list method, `.length` or for…of, or stored: const ys = xs.map(…).
|
|
483
|
+
if (this.isPipeline(node))
|
|
484
|
+
this.fail(node, `${method} builds a new list: store it (const ys = xs.${method}(…)), or use it right away with a list method, .length or for…of`);
|
|
479
485
|
// Vec<T> instruction arguments are read-only views.
|
|
480
486
|
if (ts.isIdentifier(callee.expression)) {
|
|
481
487
|
const l = this.lookup(callee.expression.text);
|
|
@@ -620,6 +626,21 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
620
626
|
this.fail(node, "only Vecs of Mut<...> / Init<...> accounts can grow or shrink");
|
|
621
627
|
this.line(`TS_TRY(ts_resize(params, ${payer}, a_${v.account.name}, 8 + ${info.fixed} + (${newLen}) * ${info.elemSize}, entry_${v.account.name}));`);
|
|
622
628
|
};
|
|
629
|
+
/**
|
|
630
|
+
* The growable Vecs stored after this one move by `by` bytes (+ grows, - shrinks): where they start and how many
|
|
631
|
+
* bytes they take, read before the resize; references into them cannot outlive this call.
|
|
632
|
+
*/
|
|
633
|
+
const tail = info.tail;
|
|
634
|
+
const tailStart = () => {
|
|
635
|
+
if (!tail)
|
|
636
|
+
return undefined;
|
|
637
|
+
this.assertNotAliased(tail.paths, node);
|
|
638
|
+
const at = this.tmp("tail");
|
|
639
|
+
const n = this.tmp("tail_n");
|
|
640
|
+
this.line(`uint8_t *${at} = ${tail.end};`);
|
|
641
|
+
this.line(`uint64_t ${n} = (uint64_t)(${tail.dataEnd} - ${at});`);
|
|
642
|
+
return { at, n };
|
|
643
|
+
};
|
|
623
644
|
switch (method) {
|
|
624
645
|
case "set": {
|
|
625
646
|
if (args.length !== 2)
|
|
@@ -655,8 +676,11 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
655
676
|
const write = stageElem(args[0]);
|
|
656
677
|
const n = this.tmp("n");
|
|
657
678
|
this.line(`uint64_t ${n} = (uint64_t)${info.len};`);
|
|
658
|
-
|
|
679
|
+
const t = tailStart();
|
|
680
|
+
// Resizing does not move the data, so element pointers stay valid; the Vecs after this one shift up.
|
|
659
681
|
resize(`${n} + 1`, `a_${payer.name}`);
|
|
682
|
+
if (t)
|
|
683
|
+
this.line(`ts_memmove(${t.at} + ${info.elemSize}, ${t.at}, ${t.n});`);
|
|
660
684
|
this.line(`${info.len} = (uint32_t)(${n} + 1);`);
|
|
661
685
|
write(elemRef(n));
|
|
662
686
|
return undefined;
|
|
@@ -686,6 +710,9 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
686
710
|
this.line(`${valueC(st)} ${out} = ${val.c};`);
|
|
687
711
|
result = st.kind === "pubkey" ? { c: `(&${out})`, type: st } : { c: out, type: st };
|
|
688
712
|
}
|
|
713
|
+
const t = info.cap === undefined ? tailStart() : undefined;
|
|
714
|
+
if (t)
|
|
715
|
+
this.line(`ts_memmove(${t.at} - ${info.elemSize}, ${t.at}, ${t.n});`);
|
|
689
716
|
this.line(`${info.len} = (uint32_t)(${n} - 1);`);
|
|
690
717
|
resize(`${n} - 1`, "NULL");
|
|
691
718
|
return result;
|
|
@@ -695,6 +722,9 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
695
722
|
this.fail(node, "clear() takes no arguments");
|
|
696
723
|
needWritable();
|
|
697
724
|
this.assertNotAliased(elemPaths(), node);
|
|
725
|
+
const t = info.cap === undefined ? tailStart() : undefined;
|
|
726
|
+
if (t)
|
|
727
|
+
this.line(`ts_memmove((uint8_t *)${info.items}, ${t.at}, ${t.n});`);
|
|
698
728
|
this.line(`${info.len} = 0;`);
|
|
699
729
|
resize("0", "NULL");
|
|
700
730
|
return undefined;
|
|
@@ -933,9 +963,10 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
933
963
|
if (this.model.events.has(s.name) && s.fields.some((f) => f.type.kind === "option" || f.type.kind === "dataEnum"))
|
|
934
964
|
this.fail(node, `${s.name} is emitted as an event and has Option or enum fields; declare a separate event struct`);
|
|
935
965
|
this.model.accountStructs.add(s.name);
|
|
936
|
-
this.line(`TS_TRY(ts_check_program_account(${h}, params->program_id, TS_DISC_${s.name},
|
|
966
|
+
this.line(`TS_TRY(ts_check_program_account(${h}, params->program_id, TS_DISC_${s.name}, ${emptySizeC(s)}));`);
|
|
937
967
|
if (s.vec)
|
|
938
|
-
|
|
968
|
+
for (const l of vecLenCheck(s, h, `((TSS_${s.name} *)(${h}->data + 8))`, ""))
|
|
969
|
+
this.line(l);
|
|
939
970
|
if (mut) {
|
|
940
971
|
this.line(`TS_TRY(ts_check_writable(${h}));`);
|
|
941
972
|
this.line(`TS_TRY(ts_check_unaliased(params, ${h}));`);
|
|
@@ -967,6 +998,37 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
967
998
|
visit(body);
|
|
968
999
|
return found;
|
|
969
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* A helper given a closure (a function that reads the caller's variables) for a function parameter: a closure only
|
|
1003
|
+
* runs where it is declared, so the helper is expanded here, like a closure, with the closure bound to the
|
|
1004
|
+
* parameter. Helpers expanded so take values (not accounts) and have no type parameters.
|
|
1005
|
+
*/
|
|
1006
|
+
helperWithClosure(node) {
|
|
1007
|
+
const g = this.givenClosure(node);
|
|
1008
|
+
if (!g)
|
|
1009
|
+
return undefined;
|
|
1010
|
+
if (g.typeParams.length)
|
|
1011
|
+
this.fail(node, `${g.display} has type parameters, so it takes only functions that read nothing around them; pass the values the function needs instead`);
|
|
1012
|
+
for (const p of g.node.parameters)
|
|
1013
|
+
if (p.type && !ts.isFunctionTypeNode(p.type) && !resolveValueType(p.type, this.scope))
|
|
1014
|
+
this.fail(node, `${g.display} takes accounts or lists, so it cannot be expanded here for the function that reads the variables around it; pass the values it needs instead`);
|
|
1015
|
+
return { value: this.expandClosure({ node: g.node, name: g.display, scopes: [] }, node.arguments, node) };
|
|
1016
|
+
}
|
|
1017
|
+
/** A function given for a function parameter of an expanded helper or closure, as something to expand. */
|
|
1018
|
+
closureOf(arg) {
|
|
1019
|
+
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
|
|
1020
|
+
return { node: arg, name: "the function passed", scopes: [...this.scopes] };
|
|
1021
|
+
if (ts.isIdentifier(arg)) {
|
|
1022
|
+
const l = this.lookup(arg.text);
|
|
1023
|
+
if (l?.closure)
|
|
1024
|
+
return l.closure;
|
|
1025
|
+
const name = calleeName(arg, this.model.helperDecls, this.checker) ?? arg.text;
|
|
1026
|
+
const h = this.model.helpers.get(name) ?? this.model.generics.get(name);
|
|
1027
|
+
if (h)
|
|
1028
|
+
return { node: h.node, name: h.display, scopes: [] };
|
|
1029
|
+
}
|
|
1030
|
+
this.fail(arg, "pass a function: its name, or an arrow written here");
|
|
1031
|
+
}
|
|
970
1032
|
callHelper(node, h) {
|
|
971
1033
|
if (this.ix && !this.ix.accounts.some((a) => a.kind.kind === "system" && !a.optional) && h.params.some((p) => p.account) && this.helperGrows(h.node))
|
|
972
1034
|
this.fail(node, `${h.display}() grows a Vec (push): the instruction needs a SystemProgram parameter to top up rent`);
|
|
@@ -2,7 +2,7 @@ import ts from "typescript";
|
|
|
2
2
|
import { discriminator, snakeCase } from "../analyze.js";
|
|
3
3
|
import { borshMinSize } from "./borsh.js";
|
|
4
4
|
import { INT_INFO, isScalar, isWritable, optionKey, sameType, scalarSize, typeName, } from "../model.js";
|
|
5
|
-
import { MAX_ACCOUNTS, MAX_SEEDS, MAX_CPI_DATA, MAX_EVENT, U64, U8, BOOL, PUBKEY, STRING, cBytes, isStable, cString, scalarStorageC, optC, valueC, } from "./shared.js";
|
|
5
|
+
import { MAX_ACCOUNTS, MAX_SEEDS, MAX_CPI_DATA, MAX_EVENT, U64, U8, BOOL, PUBKEY, STRING, cBytes, isStable, cString, scalarStorageC, optC, valueC, emptySizeC, fixedVecC, vecCount, } from "./shared.js";
|
|
6
6
|
import { CallEmitter } from "./calls.js";
|
|
7
7
|
/**
|
|
8
8
|
* Bubblegum instructions that replace a leaf: discriminator, accounts in order ([label, writable, signer, optional,
|
|
@@ -321,7 +321,7 @@ export class CpiEmitter extends CallEmitter {
|
|
|
321
321
|
const b = this.tmp("bump");
|
|
322
322
|
// The account's size: the struct's (8 + its fixed size, Anchor's InitSpace for @layout borsh), or Anchor's
|
|
323
323
|
// `space = …` when given, which must hold the struct.
|
|
324
|
-
const need = `8 + sizeof(TSS_${s.name})`;
|
|
324
|
+
const need = s.layout === "borsh" ? `8 + sizeof(TSS_${s.name})` : `8 + ${emptySizeC(s)}`;
|
|
325
325
|
let space = need;
|
|
326
326
|
const spaceArg = node.arguments[2];
|
|
327
327
|
if (spaceArg) {
|
|
@@ -2642,8 +2642,46 @@ export class CpiEmitter extends CallEmitter {
|
|
|
2642
2642
|
this.fail(obj, `missing field "${f.name}" of ${s.name}`);
|
|
2643
2643
|
for (const [name, e] of given) {
|
|
2644
2644
|
const f = s.fields.find((x) => x.name === name);
|
|
2645
|
-
if (f.type.kind === "vec")
|
|
2646
|
-
|
|
2645
|
+
if (f.type.kind === "vec" && f.type.cap !== undefined) {
|
|
2646
|
+
// A FixedVec keeps its fixed layout, as in account data: u32 length, then room for N elements.
|
|
2647
|
+
const ft = f.type;
|
|
2648
|
+
const t = this.tmp("evv");
|
|
2649
|
+
this.line(`${fixedVecC(ft)} ${t};`);
|
|
2650
|
+
this.line(`ts_memzero(&${t}, sizeof(${t}));`);
|
|
2651
|
+
this.writeAggregate({ lv: t, type: ft, writable: true }, e);
|
|
2652
|
+
const n = this.fieldBytes(ft);
|
|
2653
|
+
staged.set(name, (put) => put(`&${t}`, String(n), n));
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
if (f.type.kind === "vec") {
|
|
2657
|
+
// A Vec is Borsh: u32 length, then the elements, read from a Vec argument or account data when emitted.
|
|
2658
|
+
const l = ts.isIdentifier(e) ? this.lookup(e.text) : undefined;
|
|
2659
|
+
const r = l?.type.kind === "vecArg" ? undefined : this.ref(e);
|
|
2660
|
+
const elemSize = this.fieldBytes(f.type.elem);
|
|
2661
|
+
if (l?.type.kind === "vecArg") {
|
|
2662
|
+
if (!sameType(l.type.elem, f.type.elem))
|
|
2663
|
+
this.fail(e, `expected a ${typeName(f.type)}`);
|
|
2664
|
+
const c = l.c;
|
|
2665
|
+
staged.set(name, (put) => {
|
|
2666
|
+
const n = this.tmp("vl");
|
|
2667
|
+
this.line(`uint32_t ${n} = (uint32_t)${c}.len;`);
|
|
2668
|
+
put(`&${n}`, "4");
|
|
2669
|
+
put(`(const uint8_t *)${c}.ptr`, `(uint64_t)${n} * ${elemSize}`);
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
else if (r?.vec && r.type.kind === "vec" && sameType(r.type.elem, f.type.elem)) {
|
|
2673
|
+
const info = r.vec;
|
|
2674
|
+
staged.set(name, (put) => {
|
|
2675
|
+
const n = this.tmp("vl");
|
|
2676
|
+
this.line(`uint32_t ${n} = (uint32_t)${vecCount(info)};`);
|
|
2677
|
+
put(`&${n}`, "4");
|
|
2678
|
+
put(`(const uint8_t *)${info.items}`, `(uint64_t)${n} * ${elemSize}`);
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
else
|
|
2682
|
+
this.fail(e, `expected a ${typeName(f.type)}: a Vec argument or a list in account data`);
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2647
2685
|
if (f.type.kind === "option" && f.type.inner.kind === "struct")
|
|
2648
2686
|
this.fail(obj, "Option<struct> fields are not supported in events yet");
|
|
2649
2687
|
const vt = f.type.kind === "string" ? STRING : f.type.kind === "option" || isScalar(f.type) ? f.type : undefined;
|
|
@@ -174,6 +174,8 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
174
174
|
this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
|
|
175
175
|
if (l.type.kind === "array")
|
|
176
176
|
this.fail(node, `"${node.text}" is an array; use .get(i)`);
|
|
177
|
+
if (l.type.kind === "vec")
|
|
178
|
+
this.fail(node, `"${node.text}" is a list; use .get(i) and .length`);
|
|
177
179
|
if (l.type.kind === "remaining")
|
|
178
180
|
return { c: "", type: l.type };
|
|
179
181
|
return l.type.kind === "pubkey" && !l.ptr ? { c: `(&${l.c})`, type: l.type } : { c: l.c, type: l.type };
|
|
@@ -261,6 +263,13 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
261
263
|
if (field === "length") {
|
|
262
264
|
if (this.isRemaining(node.expression))
|
|
263
265
|
return { c: `((uint64_t)(params->ka_num - ${this.ix.accounts.length}))`, type: U64 };
|
|
266
|
+
// The number of elements a map / filter chain gives: counted, nothing stored.
|
|
267
|
+
if (this.isPipeline(node.expression)) {
|
|
268
|
+
const n = this.tmp("count");
|
|
269
|
+
this.line(`uint64_t ${n} = 0;`);
|
|
270
|
+
this.eachElement(node.expression, node, { index: `__ln${this.temp++}`, ci: this.tmp("ln_i") }, () => this.line(`${n}++;`));
|
|
271
|
+
return { c: n, type: U64 };
|
|
272
|
+
}
|
|
264
273
|
const arr = this.ref(node.expression);
|
|
265
274
|
if (arr?.type.kind === "array")
|
|
266
275
|
return { c: `${arr.type.len}ULL`, type: U64 };
|
|
@@ -3,7 +3,7 @@ import { snakeCase } from "../analyze.js";
|
|
|
3
3
|
import { decodeAddress } from "../../keys.js";
|
|
4
4
|
import { INT_INFO, isScalar, isWritable, scalarSize, typeName, } from "../model.js";
|
|
5
5
|
import { borshIdentical, borshMinSize } from "./borsh.js";
|
|
6
|
-
import { MAX_ACCOUNTS, ATA_PROGRAM_BYTES, cBytes, cString, optC, fieldC, storageDecl, helperSignature } from "./shared.js";
|
|
6
|
+
import { MAX_ACCOUNTS, ATA_PROGRAM_BYTES, cBytes, cString, optC, fieldC, storageDecl, helperSignature, emptySizeC, vecLenCheck } from "./shared.js";
|
|
7
7
|
import { StatementEmitter } from "./statements.js";
|
|
8
8
|
/**
|
|
9
9
|
* One instruction or helper as a C function: the instruction prologue (account checks, constraints and
|
|
@@ -42,7 +42,7 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
42
42
|
head.push(` TS_TRY(ts_check_writable(${v}));`);
|
|
43
43
|
// A @layout borsh account only has to hold its smallest Borsh form (unpacking checks the rest).
|
|
44
44
|
if (k.kind === "account")
|
|
45
|
-
head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, ${k.struct.layout === "borsh" ? borshMinSize(k.struct, this.model) :
|
|
45
|
+
head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.struct.name}, ${k.struct.layout === "borsh" ? borshMinSize(k.struct, this.model) : emptySizeC(k.struct)}));`);
|
|
46
46
|
// A token account created by this instruction is checked where it is created (create/createIfNeeded).
|
|
47
47
|
if (k.kind === "token" && !k.init)
|
|
48
48
|
head.push(` TS_TRY(ts_check_token_account(${v}));`);
|
|
@@ -106,7 +106,7 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
106
106
|
if (k.struct.vec) {
|
|
107
107
|
head.push(` uint64_t entry_${a.name} = ${v}->data_len;`, ` (void)entry_${a.name};`);
|
|
108
108
|
if (k.kind === "account")
|
|
109
|
-
head.push(
|
|
109
|
+
head.push(...vecLenCheck(k.struct, v, `d_${a.name}`));
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
// realloc(x, …) needs the size the account had when the instruction started (the 10 KiB growth limit).
|
|
@@ -219,6 +219,8 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
219
219
|
const size = this.fieldBytes(elem);
|
|
220
220
|
need("4");
|
|
221
221
|
head.push(` TS_VecArg arg_${a.name};`, ` arg_${a.name}.len = (uint32_t)ts_read_le(${at}, 4);`, ` off += 4;`);
|
|
222
|
+
if (a.type.cap !== undefined)
|
|
223
|
+
head.push(` if (arg_${a.name}.len > ${a.type.cap}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: more elements than the FixedVec argument holds");`);
|
|
222
224
|
need(`(uint64_t)arg_${a.name}.len * ${size}`);
|
|
223
225
|
head.push(` arg_${a.name}.ptr = ${at};`, ` off += (uint64_t)arg_${a.name}.len * ${size};`);
|
|
224
226
|
// Struct elements are read in place, so check them all now; scalars are checked when read.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_ACCOUNTS, cBytes, scalarStorageC, optC, storageDecl, helperSignature, fixedVecC } from "./shared.js";
|
|
1
|
+
import { MAX_ACCOUNTS, cBytes, scalarStorageC, optC, storageDecl, helperSignature, fixedVecC, laterVecC } from "./shared.js";
|
|
2
2
|
import { FunctionEmitter } from "./function.js";
|
|
3
3
|
import { emitBorshCodecs } from "./borsh.js";
|
|
4
4
|
/**
|
|
@@ -96,9 +96,14 @@ export function emitProgram(analysis, opts = {}) {
|
|
|
96
96
|
return;
|
|
97
97
|
emitted.add(s.name);
|
|
98
98
|
s.fields.forEach((f) => deps(f.type));
|
|
99
|
+
// Growable Vecs after the first live past the C struct (see StructDef.vecs): a header type each.
|
|
100
|
+
const later = new Set((s.vecs ?? []).slice(1).map((v) => v.name));
|
|
101
|
+
for (const v of (s.vecs ?? []).slice(1))
|
|
102
|
+
out.push(`typedef struct __attribute__((packed)) {`, ` uint32_t len;`, ` ${storageDecl(v.elem, "items[]")};`, `} ${laterVecC(s.name, v.name)};`);
|
|
99
103
|
out.push(`typedef struct __attribute__((packed)) {`);
|
|
100
104
|
for (const f of s.fields)
|
|
101
|
-
|
|
105
|
+
if (!later.has(f.name))
|
|
106
|
+
out.push(` ${storageDecl(f.type, f.name)};`);
|
|
102
107
|
if (!s.fields.length)
|
|
103
108
|
out.push(` uint8_t __empty[0];`); // a marker account (Anchor `pub struct NftAuthority {}`): discriminator only
|
|
104
109
|
out.push(`} TSS_${s.name};`);
|
|
@@ -113,6 +118,8 @@ export function emitProgram(analysis, opts = {}) {
|
|
|
113
118
|
emitStruct(s);
|
|
114
119
|
for (const e of model.dataEnums.values())
|
|
115
120
|
deps({ kind: "dataEnum", name: e.name });
|
|
121
|
+
for (const t of model.localVecs.values())
|
|
122
|
+
deps(t);
|
|
116
123
|
for (const k of model.constants.values())
|
|
117
124
|
if (typeof k.value === "object")
|
|
118
125
|
out.push(`static const SolPubkey TSK_${k.name} = {${cBytes(k.value.pubkey)}};`);
|