@typesol/cli 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +13 -12
- package/SECURITY.md +1 -1
- package/dist/clientgen.js +190 -41
- package/dist/compiler/analyze.js +111 -19
- package/dist/compiler/emit/base.d.ts +17 -4
- package/dist/compiler/emit/base.js +60 -15
- package/dist/compiler/emit/calls.d.ts +7 -0
- package/dist/compiler/emit/calls.js +99 -9
- package/dist/compiler/emit/cpi.d.ts +23 -0
- package/dist/compiler/emit/cpi.js +232 -1
- package/dist/compiler/emit/expressions.d.ts +9 -1
- package/dist/compiler/emit/expressions.js +16 -12
- package/dist/compiler/emit/function.js +3 -1
- package/dist/compiler/emit/shared.d.ts +11 -1
- package/dist/compiler/emit/statements.d.ts +4 -2
- package/dist/compiler/emit/statements.js +128 -27
- package/dist/compiler/emit/storage.d.ts +6 -2
- package/dist/compiler/emit/storage.js +36 -32
- package/dist/compiler/model.d.ts +8 -0
- package/dist/idlpublish.d.ts +6 -1
- package/dist/idlpublish.js +47 -5
- package/dist/portgen.js +1 -1
- package/dist/svmrpc.d.ts +7 -1
- package/dist/svmrpc.js +133 -19
- package/dist/testing.js +23 -3
- package/docs/cli.md +2 -2
- package/docs/client.md +13 -10
- package/docs/errors.md +9 -3
- package/docs/language.md +18 -17
- package/docs/mainnet.md +1 -1
- package/docs/migrate-from-anchor.md +8 -9
- package/docs/test-your-program.md +6 -2
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +3 -2
- package/lib/typesol.d.ts +120 -21
- package/package.json +1 -1
- package/runtime/typesol_rt.h +54 -18
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { MAX_RETURN_DATA, resolveScalarType, resolveValueType, structNameOf, typesolName } from "../analyze.js";
|
|
3
|
-
import { INT_INFO, isWritable, optionKey, scalarSize, typeName } from "../model.js";
|
|
2
|
+
import { MAX_RETURN_DATA, calleeName, resolveScalarType, resolveValueType, structNameOf, typesolName } from "../analyze.js";
|
|
3
|
+
import { INT_INFO, isScalar, isWritable, optionKey, scalarSize, typeName, } from "../model.js";
|
|
4
4
|
import { U64, BOOL, cString, fixedVecC, optC, scalarC, scalarStorageC, valueC } from "./shared.js";
|
|
5
5
|
import { CpiEmitter } from "./cpi.js";
|
|
6
6
|
/**
|
|
@@ -276,6 +276,15 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
276
276
|
this.loops.pop();
|
|
277
277
|
});
|
|
278
278
|
}
|
|
279
|
+
/** The function an identifier names (a closure, a local function or a helper), as something to expand; else undefined. */
|
|
280
|
+
functionNamed(id) {
|
|
281
|
+
const l = this.lookup(id.text);
|
|
282
|
+
if (l)
|
|
283
|
+
return l.closure;
|
|
284
|
+
const name = calleeName(id, this.model.helperDecls, this.checker);
|
|
285
|
+
const h = name ? (this.model.helpers.get(name) ?? this.model.generics.get(name)) : undefined;
|
|
286
|
+
return h ? { node: h.node, name: h.display, scopes: [], env: this.noEnv(), imports: h.imports } : undefined;
|
|
287
|
+
}
|
|
279
288
|
/** `xs.map(…)` / `xs.filter(…)` (or a chain of them): a list built element by element, not stored anywhere yet. */
|
|
280
289
|
isPipeline(e) {
|
|
281
290
|
return ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && (e.expression.name.text === "map" || e.expression.name.text === "filter");
|
|
@@ -343,7 +352,13 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
343
352
|
let idx = indexId;
|
|
344
353
|
let k = 0;
|
|
345
354
|
for (const st of stages) {
|
|
346
|
-
|
|
355
|
+
if (st.kind === "map") {
|
|
356
|
+
const ret = st.fn.body && !ts.isBlock(st.fn.body) ? st.fn.body : undefined;
|
|
357
|
+
const bare = ret && ts.isParenthesizedExpression(ret) ? ret.expression : ret;
|
|
358
|
+
if (bare && ts.isObjectLiteralExpression(bare))
|
|
359
|
+
this.fail(st.fn, "a map to structs is stored, with the struct type: const ys: FixedVec<MyStruct, N> = xs.map((x) => ({ … }))");
|
|
360
|
+
}
|
|
361
|
+
const closure = { node: st.fn, name: `${st.kind}'s callback`, scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
|
|
347
362
|
const v = this.expandClosure(closure, [element, idx].slice(0, st.fn.parameters.length), st.fn);
|
|
348
363
|
if (st.kind === "filter") {
|
|
349
364
|
if (!v)
|
|
@@ -393,8 +408,13 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
393
408
|
const n = args[1] && ts.isLiteralTypeNode(args[1]) && ts.isNumericLiteral(args[1].literal) ? Number(args[1].literal.text) : NaN;
|
|
394
409
|
const elemNode = args[0];
|
|
395
410
|
const sn = elemNode && structNameOf(elemNode, this.scope);
|
|
411
|
+
const fs = elemNode && typesolName(elemNode, this.scope.imports) === "FixedString" ? elemNode.typeArguments?.[0] : undefined;
|
|
412
|
+
const fcap = fs && ts.isLiteralTypeNode(fs) && ts.isNumericLiteral(fs.literal) ? Number(fs.literal.text) : NaN;
|
|
413
|
+
if (Number.isInteger(fcap) && fcap >= 1)
|
|
414
|
+
this.model.fstrings.add(fcap);
|
|
396
415
|
annotatedElem = elemNode
|
|
397
|
-
? (resolveScalarType(elemNode, this.scope) ??
|
|
416
|
+
? (resolveScalarType(elemNode, this.scope) ??
|
|
417
|
+
(sn && this.model.structs.has(sn) ? { kind: "struct", name: sn } : Number.isInteger(fcap) && fcap >= 1 ? { kind: "fstring", cap: fcap } : undefined))
|
|
398
418
|
: undefined;
|
|
399
419
|
if (!ts.isTypeReferenceNode(t) || typesolName(t, this.scope.imports) !== "FixedVec" || !annotatedElem || !Number.isInteger(n) || n < 1)
|
|
400
420
|
this.fail(t, "annotate the new list as FixedVec<T, N> (N a literal)");
|
|
@@ -415,21 +435,39 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
415
435
|
this.line(`/* ${c} */`);
|
|
416
436
|
let elem = stages.some((st) => st.kind === "map") ? undefined : this.listElem(base);
|
|
417
437
|
const cap0 = cap;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
438
|
+
// A last map to structs or FixedStrings (annotated) writes each result straight into its slot.
|
|
439
|
+
const last = stages.at(-1);
|
|
440
|
+
if (last?.kind === "map" && annotatedElem && !isScalar(annotatedElem)) {
|
|
441
|
+
const target = annotatedElem;
|
|
442
|
+
const inner = init.expression.expression;
|
|
443
|
+
this.eachElement(inner, init, { index: `__ml${this.temp++}`, ci: this.tmp("ml_i") }, (element, index) => {
|
|
444
|
+
this.line(`if (${c}.len >= ${cap0}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the new list is full");`);
|
|
445
|
+
const closure = { node: last.fn, name: "map's callback", scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
|
|
446
|
+
this.expandClosure(closure, [element, index].slice(0, last.fn.parameters.length), last.fn, {
|
|
447
|
+
lv: `${c}.items[${c}.len]`,
|
|
448
|
+
type: target,
|
|
449
|
+
writable: true,
|
|
450
|
+
});
|
|
451
|
+
this.line(`${c}.len++;`);
|
|
452
|
+
});
|
|
453
|
+
elem = target;
|
|
454
|
+
}
|
|
455
|
+
else
|
|
456
|
+
this.eachElement(init, init, { index: `__ml${this.temp++}`, ci: this.tmp("ml_i") }, (element) => {
|
|
457
|
+
if (!elem) {
|
|
458
|
+
const v = this.expr(element);
|
|
459
|
+
elem = v.type;
|
|
460
|
+
}
|
|
461
|
+
const target = annotatedElem ?? elem;
|
|
462
|
+
const sub = { lv: `${c}.items[${c}.len]`, type: target, writable: true };
|
|
463
|
+
this.line(`if (${c}.len >= ${cap0}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the new list is full");`);
|
|
464
|
+
const vt = this.storageValueType(target);
|
|
465
|
+
if (vt)
|
|
466
|
+
this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(element, vt), vt, element)).c) + ";");
|
|
467
|
+
else
|
|
468
|
+
this.writeAggregate(sub, element);
|
|
469
|
+
this.line(`${c}.len++;`);
|
|
470
|
+
});
|
|
433
471
|
const t = { kind: "vec", elem: annotatedElem ?? elem, cap: cap0 };
|
|
434
472
|
const bytes = 4 + this.fieldBytes(t.elem) * cap0;
|
|
435
473
|
if (bytes > 2048)
|
|
@@ -484,13 +522,17 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
484
522
|
* was declared with, so a block around the call that declares the same name does not change what it reads, and
|
|
485
523
|
* writes to captured `let`s are seen by the enclosing function. Returns the result (undefined for no value).
|
|
486
524
|
*/
|
|
487
|
-
expandClosure(c, args, node) {
|
|
525
|
+
expandClosure(c, args, node, into) {
|
|
488
526
|
const fn = c.node;
|
|
489
527
|
if (this.inlining.has(fn))
|
|
490
528
|
this.fail(node, `${c.name} calls itself: closures cannot be recursive`);
|
|
529
|
+
// Its types are written in its own file: they mean what that file's imports say (the arguments are the caller's).
|
|
530
|
+
const calleeScope = c.imports === this.scope.imports ? this.scope : { ...this.scope, imports: c.imports };
|
|
491
531
|
if (args.length !== fn.parameters.length)
|
|
492
532
|
this.fail(node, `${c.name}() takes ${fn.parameters.length} argument(s)`);
|
|
493
533
|
const params = new Map();
|
|
534
|
+
// Account parameters (of a helper expanded here) name the caller's accounts while the body runs.
|
|
535
|
+
const aliases = new Map();
|
|
494
536
|
fn.parameters.forEach((p, i) => {
|
|
495
537
|
if (!ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken)
|
|
496
538
|
this.fail(p, "closure parameters are plain names");
|
|
@@ -501,12 +543,22 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
501
543
|
params.set(name, { c: "", type: BOOL, mutable: false, closure: this.closureOf(arg) });
|
|
502
544
|
return;
|
|
503
545
|
}
|
|
546
|
+
if (p.type && ts.isIdentifier(arg) && !this.lookup(arg.text) && this.accounts.has(arg.text) && !resolveValueType(p.type, calleeScope)) {
|
|
547
|
+
aliases.set(name, this.accounts.get(arg.text));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
504
550
|
const c2 = `l_${name}_${this.temp++}`;
|
|
505
|
-
const annotated = p.type ? resolveValueType(p.type,
|
|
551
|
+
const annotated = p.type ? resolveValueType(p.type, calleeScope) : undefined;
|
|
506
552
|
if (!annotated && this.looksLikeStructPlace(arg)) {
|
|
507
553
|
const place = this.structPlace(arg);
|
|
508
554
|
this.line(`TSS_${place.struct} *${c2} = ${place.ptr};`);
|
|
509
|
-
params.set(name, {
|
|
555
|
+
params.set(name, {
|
|
556
|
+
c: c2,
|
|
557
|
+
type: { kind: "structRef", struct: place.struct, writable: place.writable },
|
|
558
|
+
mutable: false,
|
|
559
|
+
paths: this.pathsOf(arg),
|
|
560
|
+
decl: p,
|
|
561
|
+
});
|
|
510
562
|
return;
|
|
511
563
|
}
|
|
512
564
|
let v = this.expr(arg, annotated);
|
|
@@ -522,7 +574,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
522
574
|
this.line(`${valueC(type)}${type.kind === "account" ? "" : " "}${c2} = ${this.ownedString(v).c};`);
|
|
523
575
|
params.set(name, { c: c2, type, mutable: true });
|
|
524
576
|
});
|
|
525
|
-
const declared = fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword ? resolveValueType(fn.type,
|
|
577
|
+
const declared = fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword ? resolveValueType(fn.type, calleeScope) : undefined;
|
|
526
578
|
if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword && !declared)
|
|
527
579
|
this.fail(fn.type, "closures return integers, booleans, Pubkeys, strings, enums or Options");
|
|
528
580
|
const frame = {
|
|
@@ -532,10 +584,34 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
532
584
|
declAt: this.lines.length,
|
|
533
585
|
pad: " ".repeat(this.indent),
|
|
534
586
|
node: fn,
|
|
587
|
+
into,
|
|
535
588
|
};
|
|
536
589
|
this.line(declared ? `${declared.kind === "pubkey" ? "SolPubkey" : valueC(declared)} ${frame.result};` : `/* ${frame.result} */`);
|
|
537
|
-
|
|
590
|
+
// The body names what it could where it was written: a closure the caller's arguments and accounts, a helper
|
|
591
|
+
// only its own parameters (its account parameters name the caller's accounts) and the module.
|
|
592
|
+
const swap = (m, to) => {
|
|
593
|
+
const before = new Map(m);
|
|
594
|
+
m.clear();
|
|
595
|
+
for (const [k, v] of to)
|
|
596
|
+
m.set(k, v);
|
|
597
|
+
return before;
|
|
598
|
+
};
|
|
599
|
+
const bodyAccounts = new Map(c.env.accounts);
|
|
600
|
+
for (const [n, a] of aliases)
|
|
601
|
+
bodyAccounts.set(n, a);
|
|
602
|
+
// Accounts are found by name before locals: a parameter named like one the body can see would read the account.
|
|
603
|
+
for (const n of params.keys())
|
|
604
|
+
if (bodyAccounts.has(n))
|
|
605
|
+
this.fail(fn.parameters.find((p) => ts.isIdentifier(p.name) && p.name.text === n) ?? node, `"${n}" shadows the account "${n}"; use another name`);
|
|
606
|
+
const saved = {
|
|
607
|
+
scope: this.scope,
|
|
608
|
+
scopes: this.scopes,
|
|
609
|
+
loops: this.loops.splice(0),
|
|
610
|
+
args: swap(this.args, c.env.args),
|
|
611
|
+
accounts: swap(this.accounts, bodyAccounts),
|
|
612
|
+
};
|
|
538
613
|
this.scopes = [...c.scopes, params];
|
|
614
|
+
this.scope = calleeScope;
|
|
539
615
|
this.inlining.add(fn);
|
|
540
616
|
this.inlineFrames.push(frame);
|
|
541
617
|
try {
|
|
@@ -551,7 +627,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
551
627
|
const ret = ts.factory.createReturnStatement(body);
|
|
552
628
|
ts.setTextRange(ret, body);
|
|
553
629
|
ret.parent = fn;
|
|
554
|
-
if (ts.isCallExpression(body) && !frame.type) {
|
|
630
|
+
if (ts.isCallExpression(body) && !frame.type && !into) {
|
|
555
631
|
const v = this.call(body, false);
|
|
556
632
|
if (v)
|
|
557
633
|
this.closureResult(v, body, frame);
|
|
@@ -566,7 +642,10 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
566
642
|
finally {
|
|
567
643
|
this.inlineFrames.pop();
|
|
568
644
|
this.inlining.delete(fn);
|
|
645
|
+
swap(this.args, saved.args);
|
|
646
|
+
swap(this.accounts, saved.accounts);
|
|
569
647
|
this.scopes = saved.scopes;
|
|
648
|
+
this.scope = saved.scope;
|
|
570
649
|
this.loops.push(...saved.loops);
|
|
571
650
|
}
|
|
572
651
|
if (!frame.type) {
|
|
@@ -656,7 +735,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
656
735
|
this.line(`}`);
|
|
657
736
|
}
|
|
658
737
|
else {
|
|
659
|
-
const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes] };
|
|
738
|
+
const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
|
|
660
739
|
const accId = method === "reduce" ? place(f.createIdentifier(`__acc${this.temp++}`)) : undefined;
|
|
661
740
|
if (accId)
|
|
662
741
|
this.scopes.at(-1).set(accId.text, { c: res, type: accType, mutable: true });
|
|
@@ -708,6 +787,17 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
708
787
|
}
|
|
709
788
|
/** `return` inside an expanded closure: the value goes to the closure's result, then past its body. */
|
|
710
789
|
closureReturn(stmt, frame) {
|
|
790
|
+
if (frame.into && stmt.expression) {
|
|
791
|
+
// Written straight into its place (an object literal for a struct, text for a FixedString, or a value).
|
|
792
|
+
const target = frame.into;
|
|
793
|
+
const vt = this.storageValueType(target.type);
|
|
794
|
+
if (vt)
|
|
795
|
+
this.line(this.writeStorage(target, this.ownedString(this.expect(this.expr(stmt.expression, vt), vt, stmt.expression)).c) + ";");
|
|
796
|
+
else
|
|
797
|
+
this.writeAggregate(target, stmt.expression);
|
|
798
|
+
this.line(`goto ${frame.label};`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
711
801
|
if (!stmt.expression) {
|
|
712
802
|
if (frame.type)
|
|
713
803
|
this.fail(stmt, "this function returns a value");
|
|
@@ -781,7 +871,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
781
871
|
c: "",
|
|
782
872
|
type: BOOL,
|
|
783
873
|
mutable: false,
|
|
784
|
-
closure: { node: d.initializer, name: d.name.text, scopes: [...this.scopes] },
|
|
874
|
+
closure: { node: d.initializer, name: d.name.text, scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports },
|
|
785
875
|
});
|
|
786
876
|
continue;
|
|
787
877
|
}
|
|
@@ -797,6 +887,15 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
797
887
|
this.fail(d.name, `"${name}" shadows the parameter "${name}"; use another name`);
|
|
798
888
|
const c = `l_${name}_${this.temp++}`;
|
|
799
889
|
const init = d.initializer;
|
|
890
|
+
// A function as a value: another name for one (`const h = g`), or the one a function returns (`adder(5n)`).
|
|
891
|
+
const made = ts.isCallExpression(init) ? this.madeFunction(init) : undefined;
|
|
892
|
+
const alias = ts.isIdentifier(init) ? this.functionNamed(init) : undefined;
|
|
893
|
+
if (made || alias) {
|
|
894
|
+
if (mutable)
|
|
895
|
+
this.fail(d, "bind functions with const");
|
|
896
|
+
this.scopes.at(-1).set(name, { c: "", type: BOOL, mutable: false, closure: (made ?? alias) });
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
800
899
|
const fn = ts.isCallExpression(init) ? this.exportName(init.expression) : undefined;
|
|
801
900
|
if (fn === "fixedArray") {
|
|
802
901
|
const t = this.localArray(init, c);
|
|
@@ -1009,6 +1108,8 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
1009
1108
|
const pieces = this.templatePieces(rhsNode);
|
|
1010
1109
|
if (pieces.text !== undefined)
|
|
1011
1110
|
this.line(`TS_TRY(ts_str_append(&${cur}, ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})));`);
|
|
1111
|
+
else if (pieces.only !== undefined)
|
|
1112
|
+
this.line(`TS_TRY(ts_str_append(&${cur}, ${pieces.only}));`);
|
|
1012
1113
|
else
|
|
1013
1114
|
this.line(`TS_TRY(ts_str_append_parts(&${cur}, ${pieces.arr}, ${pieces.n}));`);
|
|
1014
1115
|
return;
|
|
@@ -27,8 +27,12 @@ export declare abstract class StorageEmitter extends EmitterBase {
|
|
|
27
27
|
* value. Refuse when a live reference into the replaced place is still used afterwards.
|
|
28
28
|
*/
|
|
29
29
|
protected assertNotAliased(replaced: string[] | undefined, at: ts.Node, helper?: string): void;
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Whether the variable declared by `decl` is read after `at` (later in its block, or anywhere in a loop around `at`).
|
|
32
|
+
* A parameter (of a closure or helper expanded here) is read after `at` when its function reads it after `at`; when
|
|
33
|
+
* `at` is in another function (a closure or helper it calls), any read of it in its own function counts.
|
|
34
|
+
*/
|
|
35
|
+
protected usedAfter(decl: ts.VariableDeclaration | ts.ParameterDeclaration, at: ts.Node): boolean;
|
|
32
36
|
/**
|
|
33
37
|
* A pointer to a struct in storage, from `place` or `cond ? placeA : placeB` (same struct type).
|
|
34
38
|
* Returns undefined when the expression is not such a place.
|
|
@@ -73,16 +73,7 @@ export class StorageEmitter extends EmitterBase {
|
|
|
73
73
|
return r?.type.kind === "dataEnum" ? r : undefined;
|
|
74
74
|
}
|
|
75
75
|
looksLikeDataEnum(node) {
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
return this.reporter.guard(() => this.ref(node))?.type.kind === "dataEnum";
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
this.lines.length = saved.lines;
|
|
82
|
-
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
83
|
-
this.temp = saved.temp;
|
|
84
|
-
this.reporter.diagnostics.length = saved.diagnostics;
|
|
85
|
-
}
|
|
76
|
+
return this.probe(() => this.ref(node))?.type.kind === "dataEnum";
|
|
86
77
|
}
|
|
87
78
|
variantIndex(enumName, lit) {
|
|
88
79
|
const e = this.model.dataEnums.get(enumName);
|
|
@@ -137,11 +128,37 @@ export class StorageEmitter extends EmitterBase {
|
|
|
137
128
|
: `"${name}" still refers to ${hit}; replacing it here would change what "${name}" reads (in JS it keeps the old value). Copy the fields you need into locals first`);
|
|
138
129
|
}
|
|
139
130
|
}
|
|
140
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Whether the variable declared by `decl` is read after `at` (later in its block, or anywhere in a loop around `at`).
|
|
133
|
+
* A parameter (of a closure or helper expanded here) is read after `at` when its function reads it after `at`; when
|
|
134
|
+
* `at` is in another function (a closure or helper it calls), any read of it in its own function counts.
|
|
135
|
+
*/
|
|
141
136
|
usedAfter(decl, at) {
|
|
142
137
|
const symbol = this.checker.getSymbolAtLocation(decl.name);
|
|
143
138
|
if (!symbol)
|
|
144
139
|
return true;
|
|
140
|
+
if (ts.isParameter(decl)) {
|
|
141
|
+
const fn = decl.parent;
|
|
142
|
+
const inside = at.getSourceFile() === fn.getSourceFile() && at.pos >= fn.pos && at.end <= fn.end;
|
|
143
|
+
let read = false;
|
|
144
|
+
const visitParam = (n) => {
|
|
145
|
+
if (read)
|
|
146
|
+
return;
|
|
147
|
+
if (ts.isIdentifier(n) && n !== decl.name && this.checker.getSymbolAtLocation(n) === symbol) {
|
|
148
|
+
if (!inside || n.pos >= at.end)
|
|
149
|
+
read = true;
|
|
150
|
+
// A loop inside the function, around `at`, reads it again on its next turn.
|
|
151
|
+
for (let p = at.parent; inside && p && p !== fn; p = p.parent)
|
|
152
|
+
if ((ts.isForStatement(p) || ts.isForOfStatement(p) || ts.isWhileStatement(p) || ts.isDoStatement(p)) && n.pos >= p.pos && n.end <= p.end)
|
|
153
|
+
read = true;
|
|
154
|
+
}
|
|
155
|
+
ts.forEachChild(n, visitParam);
|
|
156
|
+
};
|
|
157
|
+
const body = fn.body;
|
|
158
|
+
if (body)
|
|
159
|
+
visitParam(body);
|
|
160
|
+
return read;
|
|
161
|
+
}
|
|
145
162
|
// Loops around `at` that do not also contain the declaration run their earlier statements again later.
|
|
146
163
|
let loop;
|
|
147
164
|
for (let n = at.parent; n && n !== decl.parent; n = n.parent)
|
|
@@ -208,17 +225,8 @@ export class StorageEmitter extends EmitterBase {
|
|
|
208
225
|
return this.looksLikeStructPlace(node.expression);
|
|
209
226
|
if (ts.isConditionalExpression(node))
|
|
210
227
|
return this.looksLikeStructPlace(node.whenTrue) && this.looksLikeStructPlace(node.whenFalse);
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
const r = this.reporter.guard(() => this.ref(node));
|
|
214
|
-
return r?.type.kind === "struct";
|
|
215
|
-
}
|
|
216
|
-
finally {
|
|
217
|
-
this.lines.length = saved.lines;
|
|
218
|
-
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
219
|
-
this.temp = saved.temp;
|
|
220
|
-
this.reporter.diagnostics.length = saved.diagnostics;
|
|
221
|
-
}
|
|
228
|
+
const r = this.probe(() => this.ref(node));
|
|
229
|
+
return r?.type.kind === "struct";
|
|
222
230
|
}
|
|
223
231
|
/** `fixedArray<T, N>(fill)` as a local declaration. */
|
|
224
232
|
localArray(init, c) {
|
|
@@ -591,6 +599,11 @@ export class StorageEmitter extends EmitterBase {
|
|
|
591
599
|
const els = valueNode.elements;
|
|
592
600
|
if (els.length > t.cap)
|
|
593
601
|
this.fail(valueNode, `${els.length} elements do not fit in ${typeName(t)}`);
|
|
602
|
+
// Empty: nothing reads the old list, so it is cleared in place (length 0, every slot zero).
|
|
603
|
+
if (els.length === 0) {
|
|
604
|
+
this.line(`ts_memzero(&${target.lv}, sizeof(${target.lv}));`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
594
607
|
const tmp = this.tmp("fv");
|
|
595
608
|
this.line(`${fixedVecC(t)} ${tmp};`);
|
|
596
609
|
this.line(`ts_memzero(&${tmp}, sizeof(${tmp}));`);
|
|
@@ -681,16 +694,7 @@ export class StorageEmitter extends EmitterBase {
|
|
|
681
694
|
}
|
|
682
695
|
/** The kind of storage an expression names (struct, array, dataEnum…), without emitting code or failing. */
|
|
683
696
|
peekKind(node) {
|
|
684
|
-
|
|
685
|
-
try {
|
|
686
|
-
return this.reporter.guard(() => this.ref(node))?.type.kind;
|
|
687
|
-
}
|
|
688
|
-
finally {
|
|
689
|
-
this.lines.length = saved.lines;
|
|
690
|
-
this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
|
|
691
|
-
this.temp = saved.temp;
|
|
692
|
-
this.reporter.diagnostics.length = saved.diagnostics;
|
|
693
|
-
}
|
|
697
|
+
return this.probe(() => this.ref(node))?.type.kind;
|
|
694
698
|
}
|
|
695
699
|
place(node) {
|
|
696
700
|
if (ts.isIdentifier(node)) {
|
package/dist/compiler/model.d.ts
CHANGED
|
@@ -423,6 +423,14 @@ export interface ProgramModel {
|
|
|
423
423
|
helperDecls: Map<ts.Node, string>;
|
|
424
424
|
/** Local closures (they read the enclosing function's variables): their declaration -> name; expanded where called. */
|
|
425
425
|
closureDecls: Map<ts.Node, string>;
|
|
426
|
+
/**
|
|
427
|
+
* Functions that return a function (`function adder(n: u64): (x: u64) => u64 { return (x) => x + n; }`): no C
|
|
428
|
+
* function; a call binds the returned function, with the arguments, as a closure where it is made.
|
|
429
|
+
*/
|
|
430
|
+
factories: Map<string, {
|
|
431
|
+
node: HelperNode;
|
|
432
|
+
imports: Map<string, string>;
|
|
433
|
+
}>;
|
|
426
434
|
errors: ErrorDef[];
|
|
427
435
|
/** Enum name -> member name -> error */
|
|
428
436
|
errorEnums: Map<string, Map<string, ErrorDef>>;
|
package/dist/idlpublish.d.ts
CHANGED
|
@@ -25,7 +25,12 @@ export interface PublishOptions {
|
|
|
25
25
|
/** Write (create or update) the IDL metadata account; returns its address. */
|
|
26
26
|
export declare function publishIdl(chain: Chain, opts: PublishOptions): Promise<Address>;
|
|
27
27
|
export declare function metadataAddress(program: Address, authority: Address | null): Promise<Address>;
|
|
28
|
-
/**
|
|
28
|
+
/** The most an IDL read from the chain may inflate to: compressed data can expand far past the account's size. */
|
|
29
|
+
export declare const MAX_IDL_BYTES: number;
|
|
30
|
+
/**
|
|
31
|
+
* Read an IDL back (canonical, or the one `authority` wrote). Only an IDL stored in the metadata account itself is
|
|
32
|
+
* read: one that links to a URL or points at another account is refused (nothing is fetched from where it points).
|
|
33
|
+
*/
|
|
29
34
|
export declare function fetchIdl(rpc: Chain["rpc"], program: Address, authority?: Address | null): Promise<object>;
|
|
30
35
|
/** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
|
|
31
36
|
export declare function anchorIdlAddress(program: Address): Promise<Address>;
|
package/dist/idlpublish.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Works against an RPC (`rpcChain`) or anything else that can read accounts and send transactions (tests use LiteSVM).
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { gunzipSync, inflateSync } from "node:zlib";
|
|
10
|
+
import { Compression, DataSource, Encoding, Format, decodeData, fetchMaybeMetadata, findMetadataPda, packDirectData, writeMetadata, } from "@solana-program/program-metadata";
|
|
10
11
|
import { createAddressWithSeed, createClient, createTransactionMessage, createTransactionPlanExecutor, createTransactionPlanner, extendClient, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, getSignatureFromTransaction, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit";
|
|
11
12
|
export const PROGRAM_METADATA_ID = "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S";
|
|
12
13
|
/** A Chain over an RPC endpoint (polls signature status: no websocket needed). */
|
|
@@ -111,15 +112,54 @@ function planFailure(e) {
|
|
|
111
112
|
export async function metadataAddress(program, authority) {
|
|
112
113
|
return (await findMetadataPda({ program, authority, seed: "idl" }))[0];
|
|
113
114
|
}
|
|
114
|
-
/**
|
|
115
|
+
/** The most an IDL read from the chain may inflate to: compressed data can expand far past the account's size. */
|
|
116
|
+
export const MAX_IDL_BYTES = 16 * 1024 * 1024;
|
|
117
|
+
/** Decompress `data` (zlib or gzip), refusing output past MAX_IDL_BYTES. */
|
|
118
|
+
function inflateCapped(data, gzip, what) {
|
|
119
|
+
try {
|
|
120
|
+
return (gzip ? gunzipSync : inflateSync)(data, { maxOutputLength: MAX_IDL_BYTES });
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
if (e.code === "ERR_BUFFER_TOO_LARGE" || e instanceof RangeError)
|
|
124
|
+
throw new Error(`${what} inflates to more than ${MAX_IDL_BYTES / 1024 / 1024} MiB; refusing it`);
|
|
125
|
+
throw new Error(`${what} is not valid compressed data: ${e.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Read an IDL back (canonical, or the one `authority` wrote). Only an IDL stored in the metadata account itself is
|
|
130
|
+
* read: one that links to a URL or points at another account is refused (nothing is fetched from where it points).
|
|
131
|
+
*/
|
|
115
132
|
export async function fetchIdl(rpc, program, authority = null) {
|
|
116
|
-
|
|
133
|
+
const at = await metadataAddress(program, authority);
|
|
134
|
+
const account = await fetchMaybeMetadata(rpc, at);
|
|
135
|
+
if (!account.exists)
|
|
136
|
+
throw new Error(`no IDL metadata account at ${at}`);
|
|
137
|
+
const m = account.data;
|
|
138
|
+
if (m.dataLength > m.data.length)
|
|
139
|
+
throw new Error(`the IDL metadata at ${at} is truncated`);
|
|
140
|
+
const content = (what) => {
|
|
141
|
+
const raw = new Uint8Array(m.data.subarray(0, m.dataLength));
|
|
142
|
+
const bytes = m.compression === Compression.None ? raw : inflateCapped(raw, m.compression === Compression.Gzip, what);
|
|
143
|
+
// Base58 decodes in quadratic time: only small contents.
|
|
144
|
+
if (m.encoding === Encoding.Base58 && bytes.length > 64 * 1024)
|
|
145
|
+
throw new Error(`${what} is too large for base58`);
|
|
146
|
+
return decodeData(bytes, m.encoding);
|
|
147
|
+
};
|
|
148
|
+
if (m.dataSource === DataSource.Url) {
|
|
149
|
+
const url = content(`the IDL link at ${at}`);
|
|
150
|
+
throw new Error(`the IDL at ${at} is not stored on chain but linked: ${JSON.stringify(url.slice(0, 200))}. Fetch it yourself and pass the file (typesol client <idl.json>)`);
|
|
151
|
+
}
|
|
152
|
+
if (m.dataSource !== DataSource.Direct)
|
|
153
|
+
throw new Error(`the IDL at ${at} is stored in another account (external data), which is not read`);
|
|
154
|
+
return JSON.parse(content(`the IDL at ${at}`));
|
|
117
155
|
}
|
|
118
156
|
/** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
|
|
119
157
|
export async function anchorIdlAddress(program) {
|
|
120
158
|
const [base] = await getProgramDerivedAddress({ programAddress: program, seeds: [] });
|
|
121
159
|
return createAddressWithSeed({ baseAddress: base, programAddress: program, seed: "anchor:idl" });
|
|
122
160
|
}
|
|
161
|
+
/** Anchor's account discriminator for IdlAccount: sha256("account:IdlAccount")[0..8]. */
|
|
162
|
+
const IDL_ACCOUNT_DISCRIMINATOR = createHash("sha256").update("account:IdlAccount").digest().subarray(0, 8);
|
|
123
163
|
/** The IDL in Anchor's IDL account (`anchor idl init`), or null when the program has none. */
|
|
124
164
|
export async function fetchAnchorIdl(rpc, program) {
|
|
125
165
|
const { value } = await rpc.getAccountInfo(await anchorIdlAddress(program), { encoding: "base64" }).send();
|
|
@@ -129,10 +169,12 @@ export async function fetchAnchorIdl(rpc, program) {
|
|
|
129
169
|
// IdlAccount: 8-byte discriminator, authority (32 bytes), a u32 length, then the zlib-compressed JSON.
|
|
130
170
|
if (data.length < 44)
|
|
131
171
|
throw new Error("Anchor IDL account is too short");
|
|
172
|
+
if (!data.subarray(0, 8).equals(IDL_ACCOUNT_DISCRIMINATOR))
|
|
173
|
+
throw new Error("the account at Anchor's IDL address is not an IdlAccount");
|
|
132
174
|
const len = data.readUInt32LE(40);
|
|
133
175
|
if (44 + len > data.length)
|
|
134
176
|
throw new Error("Anchor IDL account is truncated");
|
|
135
|
-
return JSON.parse(
|
|
177
|
+
return JSON.parse(inflateCapped(data.subarray(44, 44 + len), false, "the Anchor IDL").toString("utf8"));
|
|
136
178
|
}
|
|
137
179
|
/**
|
|
138
180
|
* A program's IDL from the chain: the canonical Program Metadata IDL (`typesol idl publish`, Codama) first, else
|
package/dist/portgen.js
CHANGED
|
@@ -152,7 +152,7 @@ export function generatePort(idl) {
|
|
|
152
152
|
if (t.vec) {
|
|
153
153
|
const inner = tsType(t.vec, where, at);
|
|
154
154
|
if (where === "variant" || where === "struct" || (where === "account" && !last)) {
|
|
155
|
-
notes.push(`${at}: a Vec here (TypeSOL keeps a Vec only as an account's last field or as an argument); replaced by FixedArray, so the layout differs.`);
|
|
155
|
+
notes.push(`${at}: a Vec here (TypeSOL keeps Anchor's bytes for a growable Vec only as an account's last field or as an argument); replaced by FixedArray, so the layout differs.`);
|
|
156
156
|
return `${use("FixedArray")}<${inner}, 8> /* TODO: Anchor has a Vec here */`;
|
|
157
157
|
}
|
|
158
158
|
return `${use("Vec")}<${inner}>`;
|
package/dist/svmrpc.d.ts
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* await counter.accounts.Counter.fetch(rpc, address);
|
|
9
9
|
*
|
|
10
10
|
* Transactions run as they are sent (confirmed at once). LiteSVM cannot list accounts, so `getProgramAccounts` scans
|
|
11
|
-
* the accounts transactions sent through this connection named, plus those passed to `track`.
|
|
11
|
+
* the accounts transactions sent through this connection named, plus those passed to `track`. Subscriptions see the
|
|
12
|
+
* transactions sent through this connection: `logsNotifications` replays those sent so far (failed ones too, with
|
|
13
|
+
* `err` set), then each new one, as `{ mentions: [address] }` filters them; `accountNotifications` yields the account
|
|
14
|
+
* after each later transaction that names it. Errors are in the JSON shape RPC nodes return
|
|
15
|
+
* (`{ InstructionError: [0, { Custom: 6000 }] }`).
|
|
12
16
|
*/
|
|
13
17
|
import { type Address, type Rpc, type RpcSubscriptions, type SolanaRpcApi, type SolanaRpcSubscriptionsApi } from "@solana/kit";
|
|
14
18
|
import { type LiteSVM, type TransactionMetadata } from "litesvm";
|
|
@@ -23,4 +27,6 @@ export interface SvmConnection {
|
|
|
23
27
|
/** Add accounts for `getProgramAccounts` to scan (accounts the transactions named are scanned already). */
|
|
24
28
|
track(...addresses: Address[]): void;
|
|
25
29
|
}
|
|
30
|
+
/** A LiteSVM transaction error in the JSON shape RPC nodes return: `{ InstructionError: [0, { Custom: 6000 }] }`. */
|
|
31
|
+
export declare function rpcError(err: unknown): unknown;
|
|
26
32
|
export declare function svmConnection(svm: LiteSVM): SvmConnection;
|