@typesol/cli 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +10 -6
- package/dist/cli.js +147 -20
- package/dist/compiler/analyze.d.ts +4 -0
- package/dist/compiler/analyze.js +51 -9
- package/dist/compiler/emit/base.js +1 -1
- package/dist/compiler/emit/calls.js +7 -0
- package/dist/compiler/emit/cpi.js +188 -9
- package/dist/compiler/emit/expressions.d.ts +11 -0
- package/dist/compiler/emit/expressions.js +107 -2
- package/dist/compiler/emit/function.js +11 -1
- package/dist/compiler/emit/shared.js +3 -2
- package/dist/compiler/emit/statements.d.ts +13 -0
- package/dist/compiler/emit/statements.js +131 -5
- package/dist/compiler/emit/storage.d.ts +2 -1
- package/dist/compiler/emit/storage.js +24 -7
- package/dist/compiler/idl.js +28 -14
- package/dist/compiler/model.d.ts +16 -3
- 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 +311 -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/language.md +11 -10
- package/docs/mainnet.md +5 -4
- package/docs/migrate-from-anchor.md +104 -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 +68 -12
- package/package.json +19 -1
- package/runtime/typesol_rt.h +25 -5
- package/ts-plugin/index.js +112 -0
- package/ts-plugin/package.json +4 -0
|
@@ -50,6 +50,8 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
50
50
|
return this.switchStatement(stmt);
|
|
51
51
|
if (ts.isForStatement(stmt))
|
|
52
52
|
return this.forLoop(stmt);
|
|
53
|
+
if (ts.isForOfStatement(stmt))
|
|
54
|
+
return this.forOf(stmt);
|
|
53
55
|
if (ts.isWhileStatement(stmt))
|
|
54
56
|
return this.loop(stmt.expression, stmt.statement, undefined);
|
|
55
57
|
if (ts.isBreakStatement(stmt) || ts.isContinueStatement(stmt)) {
|
|
@@ -63,8 +65,8 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
63
65
|
this.line(ts.isBreakStatement(stmt) ? `break;` : `goto ${this.loops.at(-1)};`);
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
66
|
-
if (ts.isDoStatement(stmt) || ts.
|
|
67
|
-
this.fail(stmt, "only for (init; cond; step) and while loops are supported");
|
|
68
|
+
if (ts.isDoStatement(stmt) || ts.isForInStatement(stmt))
|
|
69
|
+
this.fail(stmt, "only for (init; cond; step), for (const x of list) and while loops are supported");
|
|
68
70
|
this.fail(stmt, `${ts.SyntaxKind[stmt.kind]} is not supported in TypeSOL`);
|
|
69
71
|
}
|
|
70
72
|
returnStatement(stmt) {
|
|
@@ -77,6 +79,25 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
77
79
|
this.line(`return SUCCESS;`);
|
|
78
80
|
return;
|
|
79
81
|
}
|
|
82
|
+
if (returns.kind === "struct") {
|
|
83
|
+
if (!stmt.expression)
|
|
84
|
+
this.fail(stmt, `return a ${returns.name}`);
|
|
85
|
+
if (this.helper) {
|
|
86
|
+
// Write the struct through the caller's out pointer (a literal, a place or another helper's result).
|
|
87
|
+
this.writeAggregate({ lv: "(*ret)", type: returns, writable: true }, stmt.expression);
|
|
88
|
+
this.line(`return SUCCESS;`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Return data: the struct's fixed layout, which the IDL describes field by field.
|
|
92
|
+
const t = this.tmp("ret");
|
|
93
|
+
this.line(`TSS_${returns.name} ${t};`);
|
|
94
|
+
this.writeAggregate({ lv: t, type: returns, writable: true }, stmt.expression);
|
|
95
|
+
this.line(`sol_set_return_data((const uint8_t *)&${t}, sizeof(${t}));`);
|
|
96
|
+
for (const l of this.epilogue())
|
|
97
|
+
this.line(l);
|
|
98
|
+
this.line(`return SUCCESS;`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
80
101
|
if (!stmt.expression)
|
|
81
102
|
this.fail(stmt, `return a ${typeName(returns)}`);
|
|
82
103
|
const v = this.expect(this.expr(stmt.expression, returns), returns, stmt.expression);
|
|
@@ -170,6 +191,65 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
170
191
|
});
|
|
171
192
|
this.line(`}`);
|
|
172
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* `for (const x of list)` over a Vec or FixedArray: an index loop that binds `x` as `const x = list.get(i)` would,
|
|
196
|
+
* so elements are values (scalars, strings) or references into the data (structs), with the same aliasing rules.
|
|
197
|
+
* As in JS, the length is read again before every step, so a body that pushes or pops sees the new length.
|
|
198
|
+
*/
|
|
199
|
+
forOf(stmt) {
|
|
200
|
+
if (stmt.awaitModifier)
|
|
201
|
+
this.fail(stmt, "for await is not supported");
|
|
202
|
+
const list = stmt.initializer;
|
|
203
|
+
const d = ts.isVariableDeclarationList(list) && list.declarations.length === 1 ? list.declarations[0] : undefined;
|
|
204
|
+
if (!d || !(list.flags & ts.NodeFlags.Const) || !ts.isIdentifier(d.name) || d.type)
|
|
205
|
+
this.fail(stmt.initializer, "write for (const x of list): one const name, no type annotation");
|
|
206
|
+
if (this.isRemaining(stmt.expression))
|
|
207
|
+
this.fail(stmt.expression, "iterate remaining accounts with for (let i = 0n; i < rest.length; i++) and rest.get(i)");
|
|
208
|
+
// Synthetic `list.length` / `list.get(i)` nodes placed at the loop, so errors and checks point at the source.
|
|
209
|
+
const f = ts.factory;
|
|
210
|
+
const index = `__of${this.temp++}`;
|
|
211
|
+
const place = (n) => {
|
|
212
|
+
ts.setTextRange(n, stmt.expression);
|
|
213
|
+
n.parent = stmt;
|
|
214
|
+
return n;
|
|
215
|
+
};
|
|
216
|
+
const indexId = place(f.createIdentifier(index));
|
|
217
|
+
const length = place(f.createPropertyAccessExpression(stmt.expression, "length"));
|
|
218
|
+
const get = place(f.createCallExpression(place(f.createPropertyAccessExpression(stmt.expression, "get")), undefined, [indexId]));
|
|
219
|
+
// The element's declaration: the source's own name (so aliasing checks find its uses) bound to list.get(i).
|
|
220
|
+
const decl = f.createVariableDeclaration(d.name, undefined, undefined, get);
|
|
221
|
+
ts.setTextRange(decl, d);
|
|
222
|
+
decl.parent = list;
|
|
223
|
+
const declList = { ...list, flags: ts.NodeFlags.Const, declarations: [decl] };
|
|
224
|
+
const ci = this.tmp("of_i");
|
|
225
|
+
const label = this.tmp("ts_continue_");
|
|
226
|
+
this.line(`{`);
|
|
227
|
+
this.block(() => {
|
|
228
|
+
this.line(`uint64_t ${ci} = 0;`);
|
|
229
|
+
this.scopes.at(-1).set(index, { c: ci, type: U64, mutable: false });
|
|
230
|
+
this.line(`while (1) {`);
|
|
231
|
+
this.block(() => {
|
|
232
|
+
const n = this.expect(this.expr(length, U64), U64, stmt.expression);
|
|
233
|
+
this.line(`if (!(${ci} < ${n.c})) break;`);
|
|
234
|
+
this.variable(declList);
|
|
235
|
+
this.loops.push(label);
|
|
236
|
+
this.line(`{`);
|
|
237
|
+
this.block(() => this.body(stmt.statement));
|
|
238
|
+
this.line(`}`);
|
|
239
|
+
this.loops.pop();
|
|
240
|
+
this.line(`${label}:;`);
|
|
241
|
+
this.line(`${ci}++;`);
|
|
242
|
+
});
|
|
243
|
+
this.line(`}`);
|
|
244
|
+
});
|
|
245
|
+
this.line(`}`);
|
|
246
|
+
}
|
|
247
|
+
/** Whether `e` is the remaining accounts (or a const bound to them). */
|
|
248
|
+
isRemaining(e) {
|
|
249
|
+
if (ts.isCallExpression(e) && this.exportName(e.expression) === "remainingAccounts")
|
|
250
|
+
return true;
|
|
251
|
+
return ts.isIdentifier(e) && this.lookup(e.text)?.type.kind === "remaining";
|
|
252
|
+
}
|
|
173
253
|
/**
|
|
174
254
|
* `while (1) { cond; if (!cond) break; body; continue_label: step }`. The condition is lowered
|
|
175
255
|
* inside the loop so its checked-arithmetic temporaries are recomputed every iteration.
|
|
@@ -204,6 +284,10 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
204
284
|
if (!(list.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)))
|
|
205
285
|
this.fail(list, "use const or let, not var");
|
|
206
286
|
for (const d of list.declarations) {
|
|
287
|
+
if (ts.isObjectBindingPattern(d.name) && d.initializer) {
|
|
288
|
+
this.destructure(list, d, d.name, d.initializer, topLevel);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
207
291
|
if (!ts.isIdentifier(d.name) || !d.initializer)
|
|
208
292
|
this.fail(d, "declare one plain name with an initializer");
|
|
209
293
|
const name = d.name.text;
|
|
@@ -312,6 +396,39 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
312
396
|
this.scopes.at(-1).set(name, { c, type, mutable });
|
|
313
397
|
}
|
|
314
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* `const { a, b: c } = s`: one declaration per name, `const a = s.a`, in source order. The source must be a name or
|
|
401
|
+
* a field path (no calls), so reading it once per name reads the same thing JS reads once.
|
|
402
|
+
*/
|
|
403
|
+
destructure(list, d, pattern, init, topLevel) {
|
|
404
|
+
if (d.type)
|
|
405
|
+
this.fail(d.type, "destructuring takes the types from the source; drop the annotation");
|
|
406
|
+
const plain = (e) => ts.isIdentifier(e) ||
|
|
407
|
+
(ts.isPropertyAccessExpression(e) && !e.questionDotToken && plain(e.expression)) ||
|
|
408
|
+
(ts.isParenthesizedExpression(e) && plain(e.expression));
|
|
409
|
+
if (!plain(init))
|
|
410
|
+
this.fail(init, "destructure a variable or a field (a.b.c); bind a call to a const first");
|
|
411
|
+
const f = ts.factory;
|
|
412
|
+
for (const el of pattern.elements) {
|
|
413
|
+
if (el.dotDotDotToken)
|
|
414
|
+
this.fail(el, "...rest is not supported in destructuring");
|
|
415
|
+
if (el.initializer)
|
|
416
|
+
this.fail(el, "default values are not supported in destructuring");
|
|
417
|
+
if (!ts.isIdentifier(el.name))
|
|
418
|
+
this.fail(el, "nested destructuring is not supported; destructure in two steps");
|
|
419
|
+
const prop = el.propertyName ?? el.name;
|
|
420
|
+
if (!ts.isIdentifier(prop))
|
|
421
|
+
this.fail(el, "use plain field names");
|
|
422
|
+
const access = f.createPropertyAccessExpression(init, prop.text);
|
|
423
|
+
ts.setTextRange(access, el);
|
|
424
|
+
access.parent = d;
|
|
425
|
+
// The element's own name node, so aliasing checks and the type checker find its uses.
|
|
426
|
+
const decl = f.createVariableDeclaration(el.name, undefined, undefined, access);
|
|
427
|
+
ts.setTextRange(decl, el);
|
|
428
|
+
decl.parent = list;
|
|
429
|
+
this.variable({ ...list, declarations: [decl] }, topLevel);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
315
432
|
expressionStatement(e, topLevel) {
|
|
316
433
|
if (ts.isBinaryExpression(e)) {
|
|
317
434
|
const op = e.operatorToken.kind;
|
|
@@ -332,6 +449,8 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
332
449
|
const base = compound[op];
|
|
333
450
|
if (base !== undefined)
|
|
334
451
|
return this.assign(e.left, e.right, base);
|
|
452
|
+
if (op === ts.SyntaxKind.AsteriskAsteriskEqualsToken)
|
|
453
|
+
this.fail(e.operatorToken, "write x = x ** n instead of **=");
|
|
335
454
|
}
|
|
336
455
|
if ((ts.isPostfixUnaryExpression(e) || ts.isPrefixUnaryExpression(e)) &&
|
|
337
456
|
(e.operator === ts.SyntaxKind.PlusPlusToken || e.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
@@ -384,8 +503,14 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
384
503
|
let raw = createsFirst ? this.call(valueNode, topLevel) : undefined;
|
|
385
504
|
if (createsFirst && !raw)
|
|
386
505
|
this.fail(valueNode, "this call does not produce a value");
|
|
506
|
+
// A helper's struct result is copied like any struct (the target first, as JS evaluates it first).
|
|
507
|
+
const structCall = isCall && ts.isIdentifier(valueNode.expression)
|
|
508
|
+
? this.model.helpers.get(valueNode.expression.text)?.returns?.kind === "struct"
|
|
509
|
+
: false;
|
|
510
|
+
if (structCall && this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
|
|
511
|
+
this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
|
|
387
512
|
// Whole arrays / structs: copy between identical layouts.
|
|
388
|
-
const targetRef = isCall ? undefined : this.ref(target);
|
|
513
|
+
const targetRef = isCall && !structCall ? undefined : this.ref(target);
|
|
389
514
|
if (targetRef &&
|
|
390
515
|
(targetRef.type.kind === "array" ||
|
|
391
516
|
targetRef.type.kind === "struct" ||
|
|
@@ -407,8 +532,9 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
407
532
|
this.fail(valueNode, "this call does not produce a value");
|
|
408
533
|
}
|
|
409
534
|
let value = this.expect(raw ?? this.expr(valueNode, place.type), place.type, valueNode);
|
|
410
|
-
|
|
411
|
-
|
|
535
|
+
// A local, or a string field of a variable enum (a view), keeps its own copy of account text.
|
|
536
|
+
if (ts.isIdentifier(target) || (value.type.kind === "string" && this.peekKind(target) === "string"))
|
|
537
|
+
value = this.ownedString(value);
|
|
412
538
|
this.line(place.write(value.c) + ";");
|
|
413
539
|
}
|
|
414
540
|
/** Whether a place goes through a Vec element (`v.get(i).x`, `v.get(i)`). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { type FieldType, type ValueType } from "../model.js";
|
|
2
|
+
import { type FieldType, type Helper, type ValueType } from "../model.js";
|
|
3
3
|
import { type Value, type Ref } from "./shared.js";
|
|
4
4
|
import { EmitterBase } from "./base.js";
|
|
5
5
|
/**
|
|
@@ -7,6 +7,7 @@ import { EmitterBase } from "./base.js";
|
|
|
7
7
|
* argument validation.
|
|
8
8
|
*/
|
|
9
9
|
export declare abstract class StorageEmitter extends EmitterBase {
|
|
10
|
+
protected abstract callHelper(node: ts.CallExpression, h: Helper): Value | undefined;
|
|
10
11
|
/** C lines rejecting malformed fixed-layout data at `lv`: bools, enums, option tags, string lengths, enum tags. */
|
|
11
12
|
protected validation(lv: string, t: FieldType, pad: string, fail?: string): string[];
|
|
12
13
|
/** The variants TypeScript has narrowed a data-enum expression to (e.g. inside `if (a.kind === "X")`). */
|
|
@@ -73,12 +73,13 @@ export class StorageEmitter extends EmitterBase {
|
|
|
73
73
|
return r?.type.kind === "dataEnum" ? r : undefined;
|
|
74
74
|
}
|
|
75
75
|
looksLikeDataEnum(node) {
|
|
76
|
-
const saved = { lines: this.lines.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
76
|
+
const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
77
77
|
try {
|
|
78
78
|
return this.reporter.guard(() => this.ref(node))?.type.kind === "dataEnum";
|
|
79
79
|
}
|
|
80
80
|
finally {
|
|
81
81
|
this.lines.length = saved.lines;
|
|
82
|
+
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
82
83
|
this.temp = saved.temp;
|
|
83
84
|
this.reporter.diagnostics.length = saved.diagnostics;
|
|
84
85
|
}
|
|
@@ -144,7 +145,7 @@ export class StorageEmitter extends EmitterBase {
|
|
|
144
145
|
// Loops around `at` that do not also contain the declaration run their earlier statements again later.
|
|
145
146
|
let loop;
|
|
146
147
|
for (let n = at.parent; n && n !== decl.parent; n = n.parent)
|
|
147
|
-
if ((ts.isForStatement(n) || ts.isWhileStatement(n) || ts.isDoStatement(n)) && !(decl.pos >= n.pos && decl.end <= n.end))
|
|
148
|
+
if ((ts.isForStatement(n) || ts.isForOfStatement(n) || ts.isWhileStatement(n) || ts.isDoStatement(n)) && !(decl.pos >= n.pos && decl.end <= n.end))
|
|
148
149
|
loop = n;
|
|
149
150
|
let found = false;
|
|
150
151
|
const scope = decl.parent.parent.parent; // VariableDeclaration -> List -> Statement -> enclosing block
|
|
@@ -207,13 +208,14 @@ export class StorageEmitter extends EmitterBase {
|
|
|
207
208
|
return this.looksLikeStructPlace(node.expression);
|
|
208
209
|
if (ts.isConditionalExpression(node))
|
|
209
210
|
return this.looksLikeStructPlace(node.whenTrue) && this.looksLikeStructPlace(node.whenFalse);
|
|
210
|
-
const saved = { lines: this.lines.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
211
|
+
const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
211
212
|
try {
|
|
212
213
|
const r = this.reporter.guard(() => this.ref(node));
|
|
213
214
|
return r?.type.kind === "struct";
|
|
214
215
|
}
|
|
215
216
|
finally {
|
|
216
217
|
this.lines.length = saved.lines;
|
|
218
|
+
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
217
219
|
this.temp = saved.temp;
|
|
218
220
|
this.reporter.diagnostics.length = saved.diagnostics;
|
|
219
221
|
}
|
|
@@ -241,6 +243,14 @@ export class StorageEmitter extends EmitterBase {
|
|
|
241
243
|
if (ts.isParenthesizedExpression(node))
|
|
242
244
|
return this.ref(node.expression);
|
|
243
245
|
this.noOptionalChain(node);
|
|
246
|
+
// A helper's struct result: a local of the caller's, read and copied like any struct.
|
|
247
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
248
|
+
const h = this.model.helpers.get(node.expression.text);
|
|
249
|
+
if (h?.returns?.kind === "struct" && !this.lookup(node.expression.text)) {
|
|
250
|
+
const v = this.callHelper(node, h);
|
|
251
|
+
return { lv: `(*${v.c})`, type: { kind: "struct", name: h.returns.name }, writable: true };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
244
254
|
if (ts.isIdentifier(node)) {
|
|
245
255
|
const l = this.lookup(node.text);
|
|
246
256
|
if (l && l.type.kind === "array")
|
|
@@ -284,7 +294,9 @@ export class StorageEmitter extends EmitterBase {
|
|
|
284
294
|
const vi = e.variants.findIndex((v) => v.name === n);
|
|
285
295
|
const fields = e.variants[vi]?.fields ?? [];
|
|
286
296
|
const at = fields.findIndex((x) => x.name === field);
|
|
287
|
-
|
|
297
|
+
// In the C payload a string is a TS_Str view (16 bytes); everything else has its stored size.
|
|
298
|
+
const cBytes = (t) => (t.kind === "string" ? 16 : this.fieldBytes(t));
|
|
299
|
+
const offset = fields.slice(0, Math.max(at, 0)).reduce((sum, x) => sum + cBytes(x.type), 0);
|
|
288
300
|
return { vi, name: n, f: fields[at], offset };
|
|
289
301
|
});
|
|
290
302
|
if (located.some((l) => !l.f))
|
|
@@ -412,6 +424,8 @@ export class StorageEmitter extends EmitterBase {
|
|
|
412
424
|
return { c: `(&${r.lv})`, type: r.type };
|
|
413
425
|
case "fstring":
|
|
414
426
|
return { c: `ts_fstr_view((const uint8_t *)&${r.lv}, ${r.type.cap})`, type: STRING };
|
|
427
|
+
case "string":
|
|
428
|
+
return { c: r.lv, type: STRING }; // a variable enum's text: already a view
|
|
415
429
|
case "enum":
|
|
416
430
|
return { c: r.lv, type: r.type };
|
|
417
431
|
case "option":
|
|
@@ -439,6 +453,7 @@ export class StorageEmitter extends EmitterBase {
|
|
|
439
453
|
return `${r.lv} = *(${c})`;
|
|
440
454
|
case "fstring":
|
|
441
455
|
return `TS_TRY(ts_fstr_set((uint8_t *)&${r.lv}, ${r.type.cap}, ${c}))`;
|
|
456
|
+
case "string":
|
|
442
457
|
case "enum":
|
|
443
458
|
case "option":
|
|
444
459
|
return `${r.lv} = ${c}`;
|
|
@@ -449,7 +464,7 @@ export class StorageEmitter extends EmitterBase {
|
|
|
449
464
|
storageValueType(t) {
|
|
450
465
|
if (isScalar(t))
|
|
451
466
|
return t;
|
|
452
|
-
if (t.kind === "fstring")
|
|
467
|
+
if (t.kind === "fstring" || t.kind === "string")
|
|
453
468
|
return STRING;
|
|
454
469
|
if (t.kind === "option" && t.inner.kind !== "struct")
|
|
455
470
|
return t;
|
|
@@ -510,8 +525,9 @@ export class StorageEmitter extends EmitterBase {
|
|
|
510
525
|
const f = variant.fields.find((x) => x.name === name);
|
|
511
526
|
const sub = { lv: `${tmp}.v.${variant.name}.${f.name}`, type: f.type, writable: true };
|
|
512
527
|
const vt = this.storageValueType(f.type);
|
|
528
|
+
// A string kept in a variable enum holds its own copy of account text (the account may change later).
|
|
513
529
|
if (vt)
|
|
514
|
-
this.line(this.writeStorage(sub, this.expect(this.expr(ex, vt), vt, ex).c) + ";");
|
|
530
|
+
this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(ex, vt), vt, ex)).c) + ";");
|
|
515
531
|
else
|
|
516
532
|
this.writeAggregate(sub, ex);
|
|
517
533
|
}
|
|
@@ -570,12 +586,13 @@ export class StorageEmitter extends EmitterBase {
|
|
|
570
586
|
}
|
|
571
587
|
/** The kind of storage an expression names (struct, array, dataEnum…), without emitting code or failing. */
|
|
572
588
|
peekKind(node) {
|
|
573
|
-
const saved = { lines: this.lines.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
589
|
+
const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
|
|
574
590
|
try {
|
|
575
591
|
return this.reporter.guard(() => this.ref(node))?.type.kind;
|
|
576
592
|
}
|
|
577
593
|
finally {
|
|
578
594
|
this.lines.length = saved.lines;
|
|
595
|
+
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
579
596
|
this.temp = saved.temp;
|
|
580
597
|
this.reporter.diagnostics.length = saved.diagnostics;
|
|
581
598
|
}
|
package/dist/compiler/idl.js
CHANGED
|
@@ -153,28 +153,42 @@ export function buildIdl(model, address, version = "0.1.0") {
|
|
|
153
153
|
},
|
|
154
154
|
})),
|
|
155
155
|
...[...model.enums.values()].map((e) => ({ name: e.name, type: { kind: "enum", variants: e.variants.map((v) => ({ name: v })) } })),
|
|
156
|
+
// A variable enum (it holds strings) only exists in Borsh: no fixed account-data form to describe.
|
|
156
157
|
...[...model.dataEnums.values()].flatMap((e) => [
|
|
157
158
|
{
|
|
158
159
|
name: e.name,
|
|
159
160
|
...docsOf(e.node),
|
|
160
161
|
type: {
|
|
161
162
|
kind: "enum",
|
|
162
|
-
variants: e.variants.map((v) => v.fields.length
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
fields: [
|
|
173
|
-
{ name: "tag", type: "u8" },
|
|
174
|
-
{ name: "payload", type: { array: ["u8", e.payload] } },
|
|
175
|
-
],
|
|
163
|
+
variants: e.variants.map((v) => v.fields.length
|
|
164
|
+
? {
|
|
165
|
+
name: v.name,
|
|
166
|
+
// A nested variable enum only exists in Borsh; fixed fields keep the fixed forms the bytes use.
|
|
167
|
+
fields: v.fields.map((f) => ({
|
|
168
|
+
name: snakeCase(f.name),
|
|
169
|
+
type: f.type.kind === "dataEnum" && model.dataEnums.get(f.type.name)?.variable ? { defined: { name: f.type.name } } : idlType(f.type),
|
|
170
|
+
})),
|
|
171
|
+
}
|
|
172
|
+
: { name: v.name }),
|
|
176
173
|
},
|
|
177
174
|
},
|
|
175
|
+
...(e.variable
|
|
176
|
+
? []
|
|
177
|
+
: [
|
|
178
|
+
{
|
|
179
|
+
name: fixedEnumName(e.name),
|
|
180
|
+
docs: [
|
|
181
|
+
`TypeSOL ${e.name} in account data: \`tag\` is the variant index, \`payload\` the variant's fields (Borsh layout) padded to the largest variant.`,
|
|
182
|
+
],
|
|
183
|
+
type: {
|
|
184
|
+
kind: "struct",
|
|
185
|
+
fields: [
|
|
186
|
+
{ name: "tag", type: "u8" },
|
|
187
|
+
{ name: "payload", type: { array: ["u8", e.payload] } },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
]),
|
|
178
192
|
]),
|
|
179
193
|
...[...model.options.values()].map((inner) => ({
|
|
180
194
|
name: fixedOptionName(inner),
|
package/dist/compiler/model.d.ts
CHANGED
|
@@ -278,7 +278,11 @@ export interface Instruction {
|
|
|
278
278
|
args: ArgParam[];
|
|
279
279
|
/** Fixed bytes of the arguments; strings add their length on top. */
|
|
280
280
|
minArgsSize: number;
|
|
281
|
-
|
|
281
|
+
/** Sent as return data: a scalar in Borsh, or a fixed-size struct in its fixed layout (as the IDL describes it). */
|
|
282
|
+
returns: ScalarType | {
|
|
283
|
+
kind: "struct";
|
|
284
|
+
name: string;
|
|
285
|
+
} | undefined;
|
|
282
286
|
/** account name -> seeds, recorded when the body creates or verifies a PDA with describable seeds. */
|
|
283
287
|
pdas: Map<string, IdlSeed[]>;
|
|
284
288
|
/** account name -> the program its PDA is derived under, when not this program (e.g. associated token accounts). */
|
|
@@ -294,11 +298,15 @@ export interface HelperParam {
|
|
|
294
298
|
account?: ParamKind;
|
|
295
299
|
node: ts.ParameterDeclaration;
|
|
296
300
|
}
|
|
297
|
-
/** A non-exported function (or any function of an imported file): values and accounts in, scalar or
|
|
301
|
+
/** A non-exported function (or any function of an imported file): values and accounts in; void, a scalar or a struct out. */
|
|
298
302
|
export interface Helper {
|
|
299
303
|
name: string;
|
|
300
304
|
params: HelperParam[];
|
|
301
|
-
|
|
305
|
+
/** A struct result is written through an out pointer (`TSS_<name> *ret`) that the caller provides. */
|
|
306
|
+
returns: ScalarType | {
|
|
307
|
+
kind: "struct";
|
|
308
|
+
name: string;
|
|
309
|
+
} | undefined;
|
|
302
310
|
node: ts.FunctionDeclaration;
|
|
303
311
|
/** The declaring file's `typesol` imports (local name -> export name). */
|
|
304
312
|
imports: Map<string, string>;
|
|
@@ -322,6 +330,11 @@ export interface DataEnumDef {
|
|
|
322
330
|
}[];
|
|
323
331
|
/** Largest variant size: the fixed payload in account data. */
|
|
324
332
|
payload: number;
|
|
333
|
+
/**
|
|
334
|
+
* A variant holds a `string` (directly or in a nested enum): such an enum is Borsh in instruction arguments and
|
|
335
|
+
* events and a local value otherwise, never account data or part of a fixed layout. Its C payload holds TS_Str views.
|
|
336
|
+
*/
|
|
337
|
+
variable?: boolean;
|
|
325
338
|
node: ts.TypeAliasDeclaration;
|
|
326
339
|
}
|
|
327
340
|
/** A unit-variant enum: `export enum Status { Open, Closed }`. */
|
|
@@ -14,6 +14,8 @@ export interface Toolchain {
|
|
|
14
14
|
/** Locate the Solana C SDK that ships with the Agave CLI's platform-tools. */
|
|
15
15
|
export declare function findToolchain(sbpf?: Sbpf): Toolchain;
|
|
16
16
|
/** Compile generated C to an SBF shared object (same flags as the SDK's sbf.mk for the chosen SBPF version). */
|
|
17
|
+
/** The SBF stack frame of one function. */
|
|
18
|
+
export declare const STACK_FRAME = 4096;
|
|
17
19
|
export declare function buildSharedObject(cFile: string, outSo: string, sbpf?: Sbpf, tc?: Toolchain): void;
|
|
18
20
|
/** The clang version string, for build manifests. */
|
|
19
21
|
export declare function toolchainVersion(tc?: Toolchain): string;
|
|
@@ -51,6 +51,8 @@ export function findToolchain(sbpf = "v0") {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
/** Compile generated C to an SBF shared object (same flags as the SDK's sbf.mk for the chosen SBPF version). */
|
|
54
|
+
/** The SBF stack frame of one function. */
|
|
55
|
+
export const STACK_FRAME = 4096;
|
|
54
56
|
export function buildSharedObject(cFile, outSo, sbpf = "v0", tc = findToolchain(sbpf)) {
|
|
55
57
|
const obj = outSo.replace(/\.so$/, ".o");
|
|
56
58
|
const cflags = [
|
|
@@ -58,6 +60,9 @@ export function buildSharedObject(cFile, outSo, sbpf = "v0", tc = findToolchain(
|
|
|
58
60
|
"-Wno-parentheses-equality",
|
|
59
61
|
// Generated checks may be always true/false (`a === a`, `enumByte >= 256` for a 256-variant enum); that is not an error.
|
|
60
62
|
"-Wno-tautological-compare",
|
|
63
|
+
// SBPF v0 gives every function a fixed 4 KiB stack frame; past it the program corrupts memory instead of failing.
|
|
64
|
+
// v1 and v2 grow frames dynamically, so the limit does not apply there.
|
|
65
|
+
...(sbpf === "v0" ? [`-Wframe-larger-than=${STACK_FRAME}`] : []),
|
|
61
66
|
"-O2",
|
|
62
67
|
"-fno-builtin",
|
|
63
68
|
// One section per function and constant, so the linker's --gc-sections can drop what nothing reaches.
|
|
@@ -70,7 +75,12 @@ export function buildSharedObject(cFile, outSo, sbpf = "v0", tc = findToolchain(
|
|
|
70
75
|
...(sbpf === "v0" ? [] : [`-mcpu=${sbpf}`]),
|
|
71
76
|
];
|
|
72
77
|
const incs = tc.includes.flatMap((i, n) => (n < 2 ? ["-isystem", i] : ["-I", i]));
|
|
73
|
-
|
|
78
|
+
try {
|
|
79
|
+
run(tc.clang, [...cflags, ...incs, "-o", obj, "-c", cFile]);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
throw stackError(e.message) ?? e;
|
|
83
|
+
}
|
|
74
84
|
run(tc.lld, [
|
|
75
85
|
"-z",
|
|
76
86
|
"notext",
|
|
@@ -99,6 +109,22 @@ export function buildSharedObject(cFile, outSo, sbpf = "v0", tc = findToolchain(
|
|
|
99
109
|
export function toolchainVersion(tc = findToolchain()) {
|
|
100
110
|
return execFileSync(tc.clang, ["--version"], { encoding: "utf8" }).split("\n")[0].trim();
|
|
101
111
|
}
|
|
112
|
+
/** clang's frame-size errors, in the program's terms: which instruction or helper, how much, what to do. */
|
|
113
|
+
function stackError(message) {
|
|
114
|
+
const over = [...message.matchAll(/stack frame size \((\d+)\) exceeds limit \((\d+)\) in '(\w+)'/g)];
|
|
115
|
+
if (!over.length)
|
|
116
|
+
return undefined;
|
|
117
|
+
// clang inlines helpers into their callers, so an instruction's frame includes theirs.
|
|
118
|
+
const what = (fn) => fn.startsWith("ix_") ? `instruction ${fn.slice(3)} (with the helpers inlined into it)` : fn.startsWith("fn_") ? `helper ${fn.slice(3)}()` : fn;
|
|
119
|
+
// Anything else clang reported in the same run stays in the message.
|
|
120
|
+
const rest = message
|
|
121
|
+
.split("\n")
|
|
122
|
+
.filter((l) => / error: /.test(l) && !/-Wframe-larger-than/.test(l))
|
|
123
|
+
.join("\n");
|
|
124
|
+
return new Error(over.map((m) => `${what(m[3])} needs ${m[1]} bytes of stack; SBPF v0 gives each function ${m[2]}`).join("\n") +
|
|
125
|
+
"\nKeep large structs and arrays in account data: bind them with const (a reference) instead of a typed local copy, or split the work into helpers." +
|
|
126
|
+
(rest ? `\n${rest}` : ""));
|
|
127
|
+
}
|
|
102
128
|
function run(cmd, args) {
|
|
103
129
|
try {
|
|
104
130
|
execFileSync(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deployed program read straight from a cluster, for `typesol verify --address`: its ELF bytes and who can still
|
|
3
|
+
* change them. Handles the three loaders a program can live under:
|
|
4
|
+
*
|
|
5
|
+
* - upgradeable (loader v3, what `solana program deploy` uses): the program account points at a ProgramData account
|
|
6
|
+
* holding the deploy slot, the upgrade authority (none once frozen) and then the ELF;
|
|
7
|
+
* - loader v4: one account holding the slot, the authority, the status and then the ELF;
|
|
8
|
+
* - the old non-upgradeable loader (v2): the account data is the ELF itself.
|
|
9
|
+
*/
|
|
10
|
+
import type { Rpc, SolanaRpcApi } from "@solana/kit";
|
|
11
|
+
export declare const LOADER_V2 = "BPFLoader2111111111111111111111111111111111";
|
|
12
|
+
export declare const LOADER_V3 = "BPFLoaderUpgradeab1e11111111111111111111111";
|
|
13
|
+
export declare const LOADER_V4 = "LoaderV411111111111111111111111111111111111";
|
|
14
|
+
export interface Deployed {
|
|
15
|
+
loader: "v2" | "v3" | "v4";
|
|
16
|
+
/** The ELF as stored (loader v3 keeps it zero-padded to the account's allocation). */
|
|
17
|
+
elf: Buffer;
|
|
18
|
+
/** Who can upgrade or close it; null when the program is immutable. */
|
|
19
|
+
authority: string | null;
|
|
20
|
+
/** The slot of the last deploy or upgrade (v3, v4). */
|
|
21
|
+
slot?: bigint;
|
|
22
|
+
/** Where the bytes live (the ProgramData account for v3). */
|
|
23
|
+
dataAccount: string;
|
|
24
|
+
}
|
|
25
|
+
type AccountRpc = Pick<Rpc<SolanaRpcApi>, "getAccountInfo">;
|
|
26
|
+
/** The deployed program at `programId`, or an error that says why there is none. */
|
|
27
|
+
export declare function fetchDeployed(rpc: AccountRpc, programId: string): Promise<Deployed>;
|
|
28
|
+
/** JSON with object keys sorted, so two IDLs compare by content and not by key order. */
|
|
29
|
+
export declare function canonicalJson(v: unknown): string;
|
|
30
|
+
export {};
|
package/dist/onchain.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getAddressDecoder } from "@solana/kit";
|
|
2
|
+
export const LOADER_V2 = "BPFLoader2111111111111111111111111111111111";
|
|
3
|
+
export const LOADER_V3 = "BPFLoaderUpgradeab1e11111111111111111111111";
|
|
4
|
+
export const LOADER_V4 = "LoaderV411111111111111111111111111111111111";
|
|
5
|
+
async function account(rpc, address) {
|
|
6
|
+
const { value } = await rpc.getAccountInfo(address, { encoding: "base64", commitment: "confirmed" }).send();
|
|
7
|
+
if (!value)
|
|
8
|
+
return null;
|
|
9
|
+
return { owner: value.owner, data: Buffer.from(value.data[0], "base64") };
|
|
10
|
+
}
|
|
11
|
+
const key = (data, offset) => getAddressDecoder().decode(data.subarray(offset, offset + 32));
|
|
12
|
+
/** The deployed program at `programId`, or an error that says why there is none. */
|
|
13
|
+
export async function fetchDeployed(rpc, programId) {
|
|
14
|
+
const program = await account(rpc, programId);
|
|
15
|
+
if (!program)
|
|
16
|
+
throw new Error(`no account at ${programId} on this cluster`);
|
|
17
|
+
if (program.owner === LOADER_V2)
|
|
18
|
+
return { loader: "v2", elf: program.data, authority: null, dataAccount: programId };
|
|
19
|
+
if (program.owner === LOADER_V4) {
|
|
20
|
+
// u64 slot, authority (or next version), u64 status (0 retracted, 1 deployed, 2 finalized), then the ELF.
|
|
21
|
+
if (program.data.length < 48)
|
|
22
|
+
throw new Error(`${programId} is not a loader-v4 program`);
|
|
23
|
+
const status = program.data.readBigUInt64LE(40);
|
|
24
|
+
if (status === 0n)
|
|
25
|
+
throw new Error(`${programId} is retracted (loader v4): it has no executable code`);
|
|
26
|
+
return {
|
|
27
|
+
loader: "v4",
|
|
28
|
+
elf: program.data.subarray(48),
|
|
29
|
+
authority: status === 2n ? null : key(program.data, 8),
|
|
30
|
+
slot: program.data.readBigUInt64LE(0),
|
|
31
|
+
dataAccount: programId,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (program.owner !== LOADER_V3)
|
|
35
|
+
throw new Error(`${programId} is not a program (owner ${program.owner})`);
|
|
36
|
+
// Program account: u32 state = 2, ProgramData address.
|
|
37
|
+
if (program.data.length < 36 || program.data.readUInt32LE(0) !== 2)
|
|
38
|
+
throw new Error(`${programId} is an upgradeable-loader account but not a program`);
|
|
39
|
+
const dataAccount = key(program.data, 4);
|
|
40
|
+
const pd = await account(rpc, dataAccount);
|
|
41
|
+
// ProgramData: u32 state = 3, u64 slot, Option<Pubkey> authority (1 + 32 bytes), then the ELF.
|
|
42
|
+
if (!pd || pd.owner !== LOADER_V3 || pd.data.length < 45 || pd.data.readUInt32LE(0) !== 3)
|
|
43
|
+
throw new Error(`${programId} has no program data (closed or never deployed)`);
|
|
44
|
+
return { loader: "v3", elf: pd.data.subarray(45), authority: pd.data[12] ? key(pd.data, 13) : null, slot: pd.data.readBigUInt64LE(4), dataAccount };
|
|
45
|
+
}
|
|
46
|
+
/** JSON with object keys sorted, so two IDLs compare by content and not by key order. */
|
|
47
|
+
export function canonicalJson(v) {
|
|
48
|
+
return JSON.stringify(v, (_k, x) => x && typeof x === "object" && !Array.isArray(x)
|
|
49
|
+
? Object.fromEntries(Object.keys(x)
|
|
50
|
+
.sort()
|
|
51
|
+
.map((k) => [k, x[k]]))
|
|
52
|
+
: x);
|
|
53
|
+
}
|