@typesol/cli 0.17.0 → 0.18.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 +75 -0
- package/README.md +4 -4
- package/dist/clientgen.js +2 -0
- package/dist/compiler/analyze.d.ts +10 -1
- package/dist/compiler/analyze.js +129 -45
- package/dist/compiler/emit/base.d.ts +29 -1
- package/dist/compiler/emit/base.js +79 -8
- package/dist/compiler/emit/borsh.js +71 -5
- package/dist/compiler/emit/calls.d.ts +2 -3
- package/dist/compiler/emit/calls.js +93 -15
- package/dist/compiler/emit/cpi.d.ts +30 -0
- package/dist/compiler/emit/cpi.js +955 -39
- package/dist/compiler/emit/expressions.d.ts +5 -0
- package/dist/compiler/emit/expressions.js +66 -3
- package/dist/compiler/emit/function.js +51 -6
- package/dist/compiler/emit/shared.d.ts +8 -0
- package/dist/compiler/emit/shared.js +4 -1
- package/dist/compiler/emit/statements.d.ts +21 -0
- package/dist/compiler/emit/statements.js +369 -16
- package/dist/compiler/emit/storage.d.ts +2 -0
- package/dist/compiler/emit/storage.js +28 -4
- package/dist/compiler/idl.js +4 -1
- package/dist/compiler/model.d.ts +29 -2
- package/dist/portgen.js +32 -2
- package/docs/errors.md +3 -2
- package/docs/language.md +30 -22
- package/docs/migrate-from-anchor.md +10 -2
- package/lib/typesol.d.ts +396 -21
- package/package.json +1 -1
- package/runtime/typesol_rt.h +198 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { resolveValueType } from "../analyze.js";
|
|
3
|
-
import { INT_INFO, isWritable, scalarSize, typeName } from "../model.js";
|
|
4
|
-
import { U64, BOOL, cString, scalarC, scalarStorageC, valueC } from "./shared.js";
|
|
2
|
+
import { MAX_RETURN_DATA, resolveValueType, structNameOf } from "../analyze.js";
|
|
3
|
+
import { INT_INFO, isWritable, optionKey, scalarSize, typeName } from "../model.js";
|
|
4
|
+
import { U64, BOOL, cString, optC, scalarC, scalarStorageC, valueC } from "./shared.js";
|
|
5
5
|
import { CpiEmitter } from "./cpi.js";
|
|
6
6
|
/**
|
|
7
7
|
* Statements: variables, assignment, control flow and return.
|
|
@@ -65,11 +65,16 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
65
65
|
this.line(ts.isBreakStatement(stmt) ? `break;` : `goto ${this.loops.at(-1)};`);
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
if (ts.isDoStatement(stmt)
|
|
69
|
-
this.
|
|
68
|
+
if (ts.isDoStatement(stmt))
|
|
69
|
+
return this.doLoop(stmt);
|
|
70
|
+
if (ts.isForInStatement(stmt))
|
|
71
|
+
this.fail(stmt, "for … in is not supported: use for (init; cond; step), for (const x of list), while or do … while");
|
|
70
72
|
this.fail(stmt, `${ts.SyntaxKind[stmt.kind]} is not supported in TypeSOL`);
|
|
71
73
|
}
|
|
72
74
|
returnStatement(stmt) {
|
|
75
|
+
const frame = this.inlineFrames.at(-1);
|
|
76
|
+
if (frame)
|
|
77
|
+
return this.closureReturn(stmt, frame);
|
|
73
78
|
const returns = this.helper ? this.helper.returns : this.ix.returns;
|
|
74
79
|
if (!returns) {
|
|
75
80
|
if (stmt.expression)
|
|
@@ -92,7 +97,18 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
92
97
|
const t = this.tmp("ret");
|
|
93
98
|
this.line(`TSS_${returns.name} ${t};`);
|
|
94
99
|
this.writeAggregate({ lv: t, type: returns, writable: true }, stmt.expression);
|
|
95
|
-
this.
|
|
100
|
+
if (this.model.structs.get(returns.name).layout === "borsh") {
|
|
101
|
+
// A @layout borsh struct returns its Borsh bytes, as Anchor's return data does.
|
|
102
|
+
const b = this.tmp("retb");
|
|
103
|
+
const w = this.tmp("retw");
|
|
104
|
+
this.line(`uint8_t *${b};`);
|
|
105
|
+
this.line(`TS_TRY(ts_alloc(sizeof(${t}), &${b}));`);
|
|
106
|
+
this.line(`uint64_t ${w} = 0;`);
|
|
107
|
+
this.line(`TS_TRY(ts_pack_${returns.name}(&${t}, ${b}, sizeof(${t}), &${w}));`);
|
|
108
|
+
this.line(`sol_set_return_data(${b}, ${w});`);
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
this.line(`sol_set_return_data((const uint8_t *)&${t}, sizeof(${t}));`);
|
|
96
112
|
for (const l of this.epilogue())
|
|
97
113
|
this.line(l);
|
|
98
114
|
this.line(`return SUCCESS;`);
|
|
@@ -101,8 +117,24 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
101
117
|
if (!stmt.expression)
|
|
102
118
|
this.fail(stmt, `return a ${typeName(returns)}`);
|
|
103
119
|
const v = this.expect(this.expr(stmt.expression, returns), returns, stmt.expression);
|
|
104
|
-
if (this.helper)
|
|
120
|
+
if (this.helper && returns.kind === "string") {
|
|
121
|
+
// The caller gets its own copy: the text may be account data the caller then changes.
|
|
122
|
+
this.line(`TS_TRY(ts_str_copy(${v.c}, ret));`);
|
|
123
|
+
}
|
|
124
|
+
else if (this.helper)
|
|
105
125
|
this.line(returns.kind === "pubkey" ? `*ret = *(${v.c});` : `*ret = ${v.c};`);
|
|
126
|
+
else if (returns.kind === "string") {
|
|
127
|
+
// Return data is the Borsh String: u32 length, then the bytes (at most 1024 in all).
|
|
128
|
+
const s = this.tmp("rets");
|
|
129
|
+
const b = this.tmp("retb");
|
|
130
|
+
this.line(`TS_Str ${s} = ${v.c};`);
|
|
131
|
+
this.line(`if (${s}.len > ${MAX_RETURN_DATA - 4}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: returned string longer than ${MAX_RETURN_DATA - 4} bytes");`);
|
|
132
|
+
this.line(`uint8_t *${b};`);
|
|
133
|
+
this.line(`TS_TRY(ts_alloc(4 + ${s}.len, &${b}));`);
|
|
134
|
+
this.line(`ts_write_le(${b}, ${s}.len, 4);`);
|
|
135
|
+
this.line(`ts_memcpy(${b} + 4, ${s}.ptr, ${s}.len);`);
|
|
136
|
+
this.line(`sol_set_return_data(${b}, 4 + ${s}.len);`);
|
|
137
|
+
}
|
|
106
138
|
else if (returns.kind === "pubkey")
|
|
107
139
|
this.line(`sol_set_return_data((const uint8_t *)(${v.c}), 32);`);
|
|
108
140
|
else {
|
|
@@ -146,11 +178,16 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
146
178
|
const typed = subject.literal !== undefined ? this.literal(subject.literal, U64, stmt.expression) : subject;
|
|
147
179
|
if (typed.type.kind !== "int" && typed.type.kind !== "enum")
|
|
148
180
|
this.fail(stmt.expression, "switch works on integers and enums");
|
|
149
|
-
|
|
150
|
-
|
|
181
|
+
// C cannot switch on 128-bit values: pick the case's index with comparisons, then switch on that.
|
|
182
|
+
const wide = typed.type.kind === "int" && INT_INFO[typed.type.name].bytes === 16;
|
|
151
183
|
const t = this.tmp("sw");
|
|
152
184
|
this.line(`${scalarC(typed.type)} ${t} = ${typed.c};`);
|
|
153
|
-
this.
|
|
185
|
+
const pick = wide ? this.tmp("swi") : undefined;
|
|
186
|
+
const pickAt = this.lines.length;
|
|
187
|
+
const cases = [];
|
|
188
|
+
if (pick)
|
|
189
|
+
this.line(`int ${pick} = -1;`);
|
|
190
|
+
this.line(`switch (${pick ?? t}) {`);
|
|
154
191
|
const seen = new Set();
|
|
155
192
|
for (const clause of stmt.caseBlock.clauses) {
|
|
156
193
|
if (ts.isCaseClause(clause)) {
|
|
@@ -166,7 +203,12 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
166
203
|
if (seen.has(label.c))
|
|
167
204
|
this.fail(clause.expression, "duplicate case");
|
|
168
205
|
seen.add(label.c);
|
|
169
|
-
|
|
206
|
+
if (pick) {
|
|
207
|
+
cases.push(label.c);
|
|
208
|
+
this.line(`case ${cases.length - 1}: {`);
|
|
209
|
+
}
|
|
210
|
+
else
|
|
211
|
+
this.line(`case ${label.c}: {`);
|
|
170
212
|
}
|
|
171
213
|
else
|
|
172
214
|
this.line(`default: {`);
|
|
@@ -176,6 +218,10 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
176
218
|
this.line(`}`);
|
|
177
219
|
}
|
|
178
220
|
this.line(`}`);
|
|
221
|
+
if (pick && cases.length) {
|
|
222
|
+
const pad = " ".repeat(this.indent);
|
|
223
|
+
this.lines.splice(pickAt + 1, 0, ...cases.map((c, i) => `${pad}${i ? "else " : ""}if (${t} == ${c}) ${pick} = ${i};`));
|
|
224
|
+
}
|
|
179
225
|
}
|
|
180
226
|
forLoop(stmt) {
|
|
181
227
|
this.line(`{`);
|
|
@@ -273,6 +319,290 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
273
319
|
});
|
|
274
320
|
this.line(`}`);
|
|
275
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Expand a closure where it is called: the arguments are evaluated here, in order, into the parameters (scalars,
|
|
324
|
+
* Pubkeys and strings by value, structs by reference, as JS passes objects); the body then runs with the names it
|
|
325
|
+
* was declared with, so a block around the call that declares the same name does not change what it reads, and
|
|
326
|
+
* writes to captured `let`s are seen by the enclosing function. Returns the result (undefined for no value).
|
|
327
|
+
*/
|
|
328
|
+
expandClosure(c, args, node) {
|
|
329
|
+
const fn = c.node;
|
|
330
|
+
if (this.inlining.has(fn))
|
|
331
|
+
this.fail(node, `${c.name} calls itself: closures cannot be recursive`);
|
|
332
|
+
if (args.length !== fn.parameters.length)
|
|
333
|
+
this.fail(node, `${c.name}() takes ${fn.parameters.length} argument(s)`);
|
|
334
|
+
const params = new Map();
|
|
335
|
+
fn.parameters.forEach((p, i) => {
|
|
336
|
+
if (!ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken)
|
|
337
|
+
this.fail(p, "closure parameters are plain names");
|
|
338
|
+
const arg = args[i];
|
|
339
|
+
const name = p.name.text;
|
|
340
|
+
const c2 = `l_${name}_${this.temp++}`;
|
|
341
|
+
const annotated = p.type ? resolveValueType(p.type, this.scope) : undefined;
|
|
342
|
+
if (!annotated && this.looksLikeStructPlace(arg)) {
|
|
343
|
+
const place = this.structPlace(arg);
|
|
344
|
+
this.line(`TSS_${place.struct} *${c2} = ${place.ptr};`);
|
|
345
|
+
params.set(name, { c: c2, type: { kind: "structRef", struct: place.struct, writable: place.writable }, mutable: false, paths: this.pathsOf(arg) });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
let v = this.expr(arg, annotated);
|
|
349
|
+
if (v.isNull && !annotated)
|
|
350
|
+
this.fail(arg, "give the parameter a type to pass null");
|
|
351
|
+
if (v.literal !== undefined && !annotated)
|
|
352
|
+
v = this.literal(v.literal, U64, arg);
|
|
353
|
+
const type = annotated ?? v.type;
|
|
354
|
+
v = this.expect(v, type, arg);
|
|
355
|
+
if (type.kind === "pubkey")
|
|
356
|
+
this.line(`SolPubkey ${c2} = *(${v.c});`);
|
|
357
|
+
else
|
|
358
|
+
this.line(`${valueC(type)}${type.kind === "account" ? "" : " "}${c2} = ${this.ownedString(v).c};`);
|
|
359
|
+
params.set(name, { c: c2, type, mutable: true });
|
|
360
|
+
});
|
|
361
|
+
const declared = fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword ? resolveValueType(fn.type, this.scope) : undefined;
|
|
362
|
+
if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword && !declared)
|
|
363
|
+
this.fail(fn.type, "closures return integers, booleans, Pubkeys, strings, enums or Options");
|
|
364
|
+
const frame = {
|
|
365
|
+
label: this.tmp("ts_ret_"),
|
|
366
|
+
result: this.tmp("ret"),
|
|
367
|
+
type: declared,
|
|
368
|
+
declAt: this.lines.length,
|
|
369
|
+
pad: " ".repeat(this.indent),
|
|
370
|
+
node: fn,
|
|
371
|
+
};
|
|
372
|
+
this.line(declared ? `${declared.kind === "pubkey" ? "SolPubkey" : valueC(declared)} ${frame.result};` : `/* ${frame.result} */`);
|
|
373
|
+
const saved = { scopes: this.scopes, loops: this.loops.splice(0) };
|
|
374
|
+
this.scopes = [...c.scopes, params];
|
|
375
|
+
this.inlining.add(fn);
|
|
376
|
+
this.inlineFrames.push(frame);
|
|
377
|
+
try {
|
|
378
|
+
this.line(`{`);
|
|
379
|
+
this.block(() => {
|
|
380
|
+
const body = fn.body;
|
|
381
|
+
if (ts.isBlock(body))
|
|
382
|
+
body.statements.forEach((s) => this.reporter.guard(() => this.statement(s)));
|
|
383
|
+
else {
|
|
384
|
+
// An expression body: its value is the result (a call may have none).
|
|
385
|
+
const ret = ts.factory.createReturnStatement(body);
|
|
386
|
+
ts.setTextRange(ret, body);
|
|
387
|
+
ret.parent = fn;
|
|
388
|
+
if (ts.isCallExpression(body) && !frame.type) {
|
|
389
|
+
const v = this.call(body, false);
|
|
390
|
+
if (v)
|
|
391
|
+
this.closureResult(v, body, frame);
|
|
392
|
+
}
|
|
393
|
+
else
|
|
394
|
+
this.closureReturn(ret, frame);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
this.line(`}`);
|
|
398
|
+
this.line(`${frame.label}:;`);
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
this.inlineFrames.pop();
|
|
402
|
+
this.inlining.delete(fn);
|
|
403
|
+
this.scopes = saved.scopes;
|
|
404
|
+
this.loops.push(...saved.loops);
|
|
405
|
+
}
|
|
406
|
+
if (!frame.type) {
|
|
407
|
+
if (this.lines[frame.declAt].includes(`/* ${frame.result} */`))
|
|
408
|
+
this.lines[frame.declAt] = "";
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
return frame.type.kind === "pubkey" ? { c: `(&${frame.result})`, type: frame.type } : { c: frame.result, type: frame.type };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* `xs.forEach / some / every / find / findIndex / reduce / includes / indexOf` on a Vec, FixedArray, FixedVec or Vec
|
|
415
|
+
* argument: an index loop (the length read each step, as `for … of`), the callback (an arrow written at the call)
|
|
416
|
+
* expanded with each element as `xs.get(i)` gives it (structs by reference) and, if it takes one, the index.
|
|
417
|
+
* `find` gives an Option of the element (scalar elements), `findIndex` / `indexOf` an i64 (-1 when absent).
|
|
418
|
+
*/
|
|
419
|
+
listMethod(node, method) {
|
|
420
|
+
const access = node.expression;
|
|
421
|
+
const list = access.expression;
|
|
422
|
+
const args = node.arguments;
|
|
423
|
+
const f = ts.factory;
|
|
424
|
+
const place = (n) => {
|
|
425
|
+
ts.setTextRange(n, node);
|
|
426
|
+
n.parent = node;
|
|
427
|
+
return n;
|
|
428
|
+
};
|
|
429
|
+
const valueForm = method === "includes" || method === "indexOf";
|
|
430
|
+
if (args.length !== (method === "reduce" ? 2 : 1))
|
|
431
|
+
this.fail(node, method === "reduce" ? "reduce(callback, initial) takes two arguments" : `${method}() takes one argument`);
|
|
432
|
+
const cb = args[0];
|
|
433
|
+
if (!valueForm && !ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb))
|
|
434
|
+
this.fail(cb, `pass ${method} an arrow function written here: xs.${method}((x) => …)`);
|
|
435
|
+
const fn = cb;
|
|
436
|
+
const index = `__lm${this.temp++}`;
|
|
437
|
+
const indexId = place(f.createIdentifier(index));
|
|
438
|
+
const length = place(f.createPropertyAccessExpression(list, "length"));
|
|
439
|
+
const element = place(f.createCallExpression(place(f.createPropertyAccessExpression(list, "get")), undefined, [indexId]));
|
|
440
|
+
const I64 = { kind: "int", name: "i64" };
|
|
441
|
+
// The result, declared before the loop.
|
|
442
|
+
const res = this.tmp("lm");
|
|
443
|
+
let result;
|
|
444
|
+
let elemType;
|
|
445
|
+
let accType;
|
|
446
|
+
let needle;
|
|
447
|
+
if (method === "some" || method === "includes")
|
|
448
|
+
(this.line(`bool ${res} = false;`), (result = { c: res, type: BOOL }));
|
|
449
|
+
if (method === "every")
|
|
450
|
+
(this.line(`bool ${res} = true;`), (result = { c: res, type: BOOL }));
|
|
451
|
+
if (method === "findIndex" || method === "indexOf")
|
|
452
|
+
(this.line(`int64_t ${res} = -1;`), (result = { c: res, type: I64 }));
|
|
453
|
+
if (method === "reduce") {
|
|
454
|
+
const accParam = fn.parameters[0];
|
|
455
|
+
const annotated = accParam?.type ? resolveValueType(accParam.type, this.scope) : undefined;
|
|
456
|
+
let init = this.expr(args[1], annotated);
|
|
457
|
+
if (init.literal !== undefined && !annotated)
|
|
458
|
+
init = this.literal(init.literal, U64, args[1]);
|
|
459
|
+
accType = annotated ?? init.type;
|
|
460
|
+
init = this.expect(init, accType, args[1]);
|
|
461
|
+
if (!["int", "bool", "pubkey", "string", "enum", "option"].includes(accType.kind))
|
|
462
|
+
this.fail(args[1], `reduce accumulates integers, booleans, Pubkeys, strings, enums or Options, not ${typeName(accType)}`);
|
|
463
|
+
this.line(accType.kind === "pubkey" ? `SolPubkey ${res} = *(${init.c});` : `${valueC(accType)} ${res} = ${this.ownedString(init).c};`);
|
|
464
|
+
result = accType.kind === "pubkey" ? { c: `(&${res})`, type: accType } : { c: res, type: accType };
|
|
465
|
+
}
|
|
466
|
+
if (valueForm) {
|
|
467
|
+
// The value looked for, read once, compared with === (scalars and strings).
|
|
468
|
+
const v = this.expr(cb);
|
|
469
|
+
needle = `__lv${this.temp++}`;
|
|
470
|
+
const t = this.tmp("needle");
|
|
471
|
+
const vt = v.literal !== undefined ? U64 : v.type;
|
|
472
|
+
if (!["int", "bool", "pubkey", "string", "enum"].includes(vt.kind))
|
|
473
|
+
this.fail(cb, `${method} compares integers, booleans, Pubkeys, strings or enums`);
|
|
474
|
+
const typed = v.literal !== undefined ? v : this.expect(v, vt, cb);
|
|
475
|
+
this.line(vt.kind === "pubkey" ? `SolPubkey ${t} = *(${typed.c});` : `${valueC(vt)} ${t} = ${typed.c};`);
|
|
476
|
+
this.scopes.at(-1).set(needle, { c: t, type: vt, mutable: false });
|
|
477
|
+
}
|
|
478
|
+
if (method === "find") {
|
|
479
|
+
// Typed from the first element read inside the loop; declared here.
|
|
480
|
+
result = { c: res, type: BOOL };
|
|
481
|
+
this.line(`/* ${res} */`);
|
|
482
|
+
}
|
|
483
|
+
const findDecl = method === "find" ? this.lines.length - 1 : -1;
|
|
484
|
+
const ci = this.tmp("lm_i");
|
|
485
|
+
this.line(`{`);
|
|
486
|
+
this.block(() => {
|
|
487
|
+
this.line(`uint64_t ${ci} = 0;`);
|
|
488
|
+
this.scopes.at(-1).set(index, { c: ci, type: U64, mutable: false });
|
|
489
|
+
this.line(`while (1) {`);
|
|
490
|
+
this.block(() => {
|
|
491
|
+
const n = this.expect(this.expr(length, U64), U64, list);
|
|
492
|
+
this.line(`if (!(${ci} < ${n.c})) break;`);
|
|
493
|
+
if (valueForm) {
|
|
494
|
+
const eq = place(f.createBinaryExpression(element, ts.SyntaxKind.EqualsEqualsEqualsToken, place(f.createIdentifier(needle))));
|
|
495
|
+
const hit = this.expect(this.expr(eq, BOOL), BOOL, node);
|
|
496
|
+
this.line(`if (${hit.c}) {`);
|
|
497
|
+
this.line(method === "includes" ? ` ${res} = true;` : ` ${res} = (int64_t)${ci};`);
|
|
498
|
+
this.line(` break;`);
|
|
499
|
+
this.line(`}`);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes] };
|
|
503
|
+
const accId = method === "reduce" ? place(f.createIdentifier(`__acc${this.temp++}`)) : undefined;
|
|
504
|
+
if (accId)
|
|
505
|
+
this.scopes.at(-1).set(accId.text, { c: res, type: accType, mutable: true });
|
|
506
|
+
const all = method === "reduce" ? [accId, element, indexId] : [element, indexId];
|
|
507
|
+
const v = this.expandClosure(closure, all.slice(0, fn.parameters.length), cb);
|
|
508
|
+
if (method === "forEach") {
|
|
509
|
+
if (v)
|
|
510
|
+
this.fail(cb, "forEach's callback returns nothing");
|
|
511
|
+
}
|
|
512
|
+
else if (method === "reduce") {
|
|
513
|
+
if (!v)
|
|
514
|
+
this.fail(cb, "reduce's callback returns the new accumulator");
|
|
515
|
+
const nv = this.expect(v, accType, cb);
|
|
516
|
+
this.line(accType.kind === "pubkey" ? `${res} = *(${nv.c});` : `${res} = ${nv.c};`);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
if (!v)
|
|
520
|
+
this.fail(cb, `${method}'s callback returns a boolean`);
|
|
521
|
+
const b = this.expect(v, BOOL, cb);
|
|
522
|
+
if (method === "some")
|
|
523
|
+
this.line(`if (${b.c}) { ${res} = true; break; }`);
|
|
524
|
+
if (method === "every")
|
|
525
|
+
this.line(`if (!(${b.c})) { ${res} = false; break; }`);
|
|
526
|
+
if (method === "findIndex")
|
|
527
|
+
this.line(`if (${b.c}) { ${res} = (int64_t)${ci}; break; }`);
|
|
528
|
+
if (method === "find") {
|
|
529
|
+
this.line(`if (${b.c}) {`);
|
|
530
|
+
this.block(() => {
|
|
531
|
+
const e = this.expr(element);
|
|
532
|
+
elemType = e.type;
|
|
533
|
+
if (!["int", "bool", "pubkey", "enum"].includes(e.type.kind))
|
|
534
|
+
this.fail(node, "find returns an Option of the element, so the elements must be integers, booleans, Pubkeys or enums; use findIndex");
|
|
535
|
+
this.line(`${res}.some = 1;`);
|
|
536
|
+
this.line(e.type.kind === "pubkey" ? `${res}.v = *(${e.c});` : `${res}.v = ${e.c};`);
|
|
537
|
+
this.line(`break;`);
|
|
538
|
+
});
|
|
539
|
+
this.line(`}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
this.line(`${ci}++;`);
|
|
544
|
+
});
|
|
545
|
+
this.line(`}`);
|
|
546
|
+
});
|
|
547
|
+
this.line(`}`);
|
|
548
|
+
if (method === "find") {
|
|
549
|
+
const inner = elemType;
|
|
550
|
+
this.model.options.set(optionKey(inner), inner);
|
|
551
|
+
this.lines[findDecl] = `${" ".repeat(this.indent)}${optC(inner)} ${res};\n${" ".repeat(this.indent)}ts_memzero(&${res}, sizeof(${res}));`;
|
|
552
|
+
return { c: res, type: { kind: "option", inner } };
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
/** `return` inside an expanded closure: the value goes to the closure's result, then past its body. */
|
|
557
|
+
closureReturn(stmt, frame) {
|
|
558
|
+
if (!stmt.expression) {
|
|
559
|
+
if (frame.type)
|
|
560
|
+
this.fail(stmt, "this function returns a value");
|
|
561
|
+
this.line(`goto ${frame.label};`);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
this.closureResult(this.expr(stmt.expression, frame.type), stmt.expression, frame);
|
|
565
|
+
}
|
|
566
|
+
/** Store a closure's result (fixing its type on the first one) and jump past the rest of its body. */
|
|
567
|
+
closureResult(value, at, frame) {
|
|
568
|
+
let v = value;
|
|
569
|
+
const stmt = { expression: at };
|
|
570
|
+
if (v.isNull && !frame.type)
|
|
571
|
+
this.fail(stmt.expression, "give the function a return type to return null");
|
|
572
|
+
if (!frame.type) {
|
|
573
|
+
if (v.literal !== undefined)
|
|
574
|
+
v = this.literal(v.literal, U64, stmt.expression);
|
|
575
|
+
const t = v.type;
|
|
576
|
+
if (!["int", "bool", "pubkey", "string", "enum", "option"].includes(t.kind))
|
|
577
|
+
this.fail(stmt.expression, `closures return integers, booleans, Pubkeys, strings, enums or Options, not ${typeName(t)}`);
|
|
578
|
+
frame.type = t;
|
|
579
|
+
this.lines[frame.declAt] = `${frame.pad}${t.kind === "pubkey" ? "SolPubkey" : valueC(t)} ${frame.result};`;
|
|
580
|
+
}
|
|
581
|
+
v = this.expect(v, frame.type, stmt.expression);
|
|
582
|
+
if (frame.type.kind === "pubkey")
|
|
583
|
+
this.line(`${frame.result} = *(${v.c});`);
|
|
584
|
+
else if (frame.type.kind === "option" && v.isNull)
|
|
585
|
+
this.line(`ts_memzero(&${frame.result}, sizeof(${frame.result}));`);
|
|
586
|
+
else
|
|
587
|
+
this.line(`${frame.result} = ${this.ownedString(v).c};`);
|
|
588
|
+
this.line(`goto ${frame.label};`);
|
|
589
|
+
}
|
|
590
|
+
/** `do { … } while (cond)`: the body once, then the test; `continue` goes to the test. */
|
|
591
|
+
doLoop(stmt) {
|
|
592
|
+
const label = this.tmp("ts_continue_");
|
|
593
|
+
this.line(`while (1) {`);
|
|
594
|
+
this.block(() => {
|
|
595
|
+
this.loops.push(label);
|
|
596
|
+
this.line(`{`);
|
|
597
|
+
this.block(() => this.body(stmt.statement));
|
|
598
|
+
this.line(`}`);
|
|
599
|
+
this.loops.pop();
|
|
600
|
+
this.line(`${label}:;`);
|
|
601
|
+
const c = this.expect(this.expr(stmt.expression, BOOL), BOOL, stmt.expression);
|
|
602
|
+
this.line(`if (!${c.c}) break;`);
|
|
603
|
+
});
|
|
604
|
+
this.line(`}`);
|
|
605
|
+
}
|
|
276
606
|
body(stmt) {
|
|
277
607
|
if (ts.isBlock(stmt))
|
|
278
608
|
stmt.statements.forEach((s) => this.reporter.guard(() => this.statement(s)));
|
|
@@ -292,6 +622,16 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
292
622
|
this.fail(d, "declare one plain name with an initializer");
|
|
293
623
|
// A local function is a helper of its own (lifted when the program was analyzed): nothing to run here.
|
|
294
624
|
if (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer)) {
|
|
625
|
+
// A closure: remembered with the scopes it can see, and expanded where it is called.
|
|
626
|
+
if (this.model.closureDecls.has(d)) {
|
|
627
|
+
this.scopes.at(-1).set(d.name.text, {
|
|
628
|
+
c: "",
|
|
629
|
+
type: BOOL,
|
|
630
|
+
mutable: false,
|
|
631
|
+
closure: { node: d.initializer, name: d.name.text, scopes: [...this.scopes] },
|
|
632
|
+
});
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
295
635
|
if (!this.model.helperDecls.has(d))
|
|
296
636
|
this.fail(d, "declare local functions with const: const f = (…) => …");
|
|
297
637
|
continue;
|
|
@@ -319,9 +659,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
319
659
|
continue;
|
|
320
660
|
}
|
|
321
661
|
// Struct values: `const p: Point = { x, y }` (or a copy of another Point) is a local of its own.
|
|
322
|
-
const structType = d.type &&
|
|
323
|
-
? this.model.structs.get(d.type.typeName.text)
|
|
324
|
-
: undefined;
|
|
662
|
+
const structType = d.type && structNameOf(d.type, this.scope) ? this.model.structs.get(structNameOf(d.type, this.scope)) : undefined;
|
|
325
663
|
if (structType) {
|
|
326
664
|
if (mutable)
|
|
327
665
|
this.fail(d, "bind struct values with const (their fields can still be updated)");
|
|
@@ -455,8 +793,23 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
455
793
|
const base = compound[op];
|
|
456
794
|
if (base !== undefined)
|
|
457
795
|
return this.assign(e.left, e.right, base);
|
|
458
|
-
if (op === ts.SyntaxKind.AsteriskAsteriskEqualsToken)
|
|
459
|
-
|
|
796
|
+
if (op === ts.SyntaxKind.AsteriskAsteriskEqualsToken) {
|
|
797
|
+
// x **= n is x = x ** n; the target is read twice, so it must not run anything (no calls in it).
|
|
798
|
+
let hasCall = false;
|
|
799
|
+
const visit = (n) => {
|
|
800
|
+
if (ts.isCallExpression(n) && !this.isStorageCall(n))
|
|
801
|
+
hasCall = true;
|
|
802
|
+
ts.forEachChild(n, visit);
|
|
803
|
+
};
|
|
804
|
+
visit(e.left);
|
|
805
|
+
if (hasCall)
|
|
806
|
+
this.fail(e.left, "**= on a target that calls a function: write x = x ** n with the target in a local");
|
|
807
|
+
const f = ts.factory;
|
|
808
|
+
const pow = f.createBinaryExpression(e.left, ts.SyntaxKind.AsteriskAsteriskToken, e.right);
|
|
809
|
+
ts.setTextRange(pow, e);
|
|
810
|
+
pow.parent = e;
|
|
811
|
+
return this.assign(e.left, pow);
|
|
812
|
+
}
|
|
460
813
|
}
|
|
461
814
|
if ((ts.isPostfixUnaryExpression(e) || ts.isPrefixUnaryExpression(e)) &&
|
|
462
815
|
(e.operator === ts.SyntaxKind.PlusPlusToken || e.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
@@ -66,6 +66,8 @@ export declare abstract class StorageEmitter extends EmitterBase {
|
|
|
66
66
|
* Write a struct or array into storage: from an object literal (field by field, every field
|
|
67
67
|
* required) or by copying another location of the same type.
|
|
68
68
|
*/
|
|
69
|
+
/** A whole struct account copied as a value (`return vault`, `info = vault`): fixed-size structs only. */
|
|
70
|
+
private wholeAccount;
|
|
69
71
|
protected writeAggregate(target: Ref, valueNode: ts.Expression): void;
|
|
70
72
|
/** The Vec view of a FixedVec stored at `lv` (`{ len, items[cap] }`). */
|
|
71
73
|
protected fixedVec(lv: string, elem: FieldType, cap: number): NonNullable<Ref["vec"]>;
|
|
@@ -242,6 +242,9 @@ export class StorageEmitter extends EmitterBase {
|
|
|
242
242
|
ref(node) {
|
|
243
243
|
if (ts.isParenthesizedExpression(node))
|
|
244
244
|
return this.ref(node.expression);
|
|
245
|
+
const unbanged = this.withoutBang(node);
|
|
246
|
+
if (unbanged)
|
|
247
|
+
return this.ref(unbanged);
|
|
245
248
|
this.noOptionalChain(node);
|
|
246
249
|
// A helper's struct result: a local of the caller's, read and copied like any struct.
|
|
247
250
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
@@ -276,13 +279,13 @@ export class StorageEmitter extends EmitterBase {
|
|
|
276
279
|
if (f.type.kind === "vec" && f.type.cap !== undefined)
|
|
277
280
|
r.vec = this.fixedVec(r.lv, f.type.elem, f.type.cap);
|
|
278
281
|
else if (f.type.kind === "vec")
|
|
279
|
-
// Growing needs the account's length at instruction entry:
|
|
282
|
+
// Growing needs the account's length at instruction entry: instructions have it, helpers get it passed.
|
|
280
283
|
r.vec = {
|
|
281
284
|
len: `d_${a.name}->${f.name}__len`,
|
|
282
285
|
items: r.lv,
|
|
283
286
|
fixed: `sizeof(TSS_${a.kind.struct.name})`,
|
|
284
287
|
elemSize: a.kind.struct.vec.elemSize,
|
|
285
|
-
resizable: isWritable(a.kind)
|
|
288
|
+
resizable: isWritable(a.kind),
|
|
286
289
|
};
|
|
287
290
|
return r;
|
|
288
291
|
}
|
|
@@ -337,7 +340,7 @@ export class StorageEmitter extends EmitterBase {
|
|
|
337
340
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "get") {
|
|
338
341
|
const owner = node.expression.expression;
|
|
339
342
|
const l = ts.isIdentifier(owner) ? this.lookup(owner.text) : undefined;
|
|
340
|
-
if (l?.type.kind === "vecArg" && !isScalar(l.type.elem)) {
|
|
343
|
+
if (l?.type.kind === "vecArg" && !isScalar(l.type.elem) && l.type.elem.kind !== "string") {
|
|
341
344
|
// A Vec<Struct> argument: elements are read in place (validated when the instruction started).
|
|
342
345
|
if (node.arguments.length !== 1)
|
|
343
346
|
this.fail(node, "get(index) takes one argument");
|
|
@@ -478,6 +481,16 @@ export class StorageEmitter extends EmitterBase {
|
|
|
478
481
|
* Write a struct or array into storage: from an object literal (field by field, every field
|
|
479
482
|
* required) or by copying another location of the same type.
|
|
480
483
|
*/
|
|
484
|
+
/** A whole struct account copied as a value (`return vault`, `info = vault`): fixed-size structs only. */
|
|
485
|
+
wholeAccount(node) {
|
|
486
|
+
if (!ts.isIdentifier(node) || this.lookup(node.text))
|
|
487
|
+
return undefined;
|
|
488
|
+
const a = this.accounts.get(node.text);
|
|
489
|
+
if (!a || !(a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "initIfNeeded") || a.kind.struct.size === undefined || a.kind.struct.vec)
|
|
490
|
+
return undefined;
|
|
491
|
+
this.assertUsable(a, node);
|
|
492
|
+
return { lv: `(*d_${a.name})`, type: { kind: "struct", name: a.kind.struct.name }, writable: false };
|
|
493
|
+
}
|
|
481
494
|
writeAggregate(target, valueNode) {
|
|
482
495
|
if (ts.isParenthesizedExpression(valueNode))
|
|
483
496
|
return this.writeAggregate(target, valueNode.expression);
|
|
@@ -604,7 +617,18 @@ export class StorageEmitter extends EmitterBase {
|
|
|
604
617
|
this.line(`ts_memcpy(&${target.lv}, &${tmp}, sizeof(${tmp}));`);
|
|
605
618
|
return;
|
|
606
619
|
}
|
|
607
|
-
|
|
620
|
+
if (ts.isConditionalExpression(valueNode)) {
|
|
621
|
+
// `cond ? a : b` (places, literals or calls): each branch writes the target. A branch that reads the target
|
|
622
|
+
// copies it onto itself, which the copies below allow.
|
|
623
|
+
const cond = this.expect(this.expr(valueNode.condition, BOOL), BOOL, valueNode.condition);
|
|
624
|
+
this.line(`if (${cond.c}) {`);
|
|
625
|
+
this.block(() => this.writeAggregate(target, valueNode.whenTrue));
|
|
626
|
+
this.line(`} else {`);
|
|
627
|
+
this.block(() => this.writeAggregate(target, valueNode.whenFalse));
|
|
628
|
+
this.line(`}`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const src = this.ref(valueNode) ?? this.wholeAccount(valueNode);
|
|
608
632
|
if (!src || !sameType(src.type, target.type))
|
|
609
633
|
this.fail(valueNode, `expected a ${typeName(target.type)}: an object literal or another ${typeName(target.type)}`);
|
|
610
634
|
// Same-typed places are either distinct or identical (`a.p = a.p`); ts_copy skips the latter.
|
package/dist/compiler/idl.js
CHANGED
|
@@ -45,6 +45,9 @@ function borshIdlType(t) {
|
|
|
45
45
|
return { vec: borshIdlType(t.elem) };
|
|
46
46
|
if (t.kind === "option")
|
|
47
47
|
return { option: idlType(t.inner) };
|
|
48
|
+
// A data enum in a Borsh struct is the Borsh enum itself (tag, then the variant's fields).
|
|
49
|
+
if (t.kind === "dataEnum")
|
|
50
|
+
return { defined: { name: t.name } };
|
|
48
51
|
return idlType(t);
|
|
49
52
|
}
|
|
50
53
|
function idlType(t, borshOption = false) {
|
|
@@ -90,7 +93,7 @@ function accountEntry(ix, name, k, programAddress) {
|
|
|
90
93
|
entry.writable = true;
|
|
91
94
|
if (param?.optional)
|
|
92
95
|
entry.optional = true;
|
|
93
|
-
if (k.kind === "signer")
|
|
96
|
+
if (k.kind === "signer" || param?.signsInit)
|
|
94
97
|
entry.signer = true;
|
|
95
98
|
if (k.kind === "system")
|
|
96
99
|
entry.address = SYSTEM_PROGRAM;
|
package/dist/compiler/model.d.ts
CHANGED
|
@@ -143,14 +143,22 @@ export interface StructDef {
|
|
|
143
143
|
* Anchor's bytes, where strings, Vecs and Options take only what they hold).
|
|
144
144
|
*/
|
|
145
145
|
layout: "fixed" | "borsh";
|
|
146
|
+
/**
|
|
147
|
+
* A @layout borsh struct with a `string` field or a data enum carrying strings: they are views of the bytes it was
|
|
148
|
+
* read from, so it is an argument, return value or event, never account data.
|
|
149
|
+
*/
|
|
150
|
+
viewsOnly?: boolean;
|
|
146
151
|
}
|
|
147
152
|
export type TokenProgramKind = "token" | "token2022" | "tokenInterface";
|
|
148
153
|
export type ParamKind = {
|
|
149
154
|
kind: "signer";
|
|
150
155
|
mut: boolean;
|
|
151
|
-
}
|
|
156
|
+
}
|
|
157
|
+
/** `systemOwned`: a SystemAccount (Anchor's): owned by the System Program (3011). */
|
|
158
|
+
| {
|
|
152
159
|
kind: "info";
|
|
153
160
|
mut: boolean;
|
|
161
|
+
systemOwned?: boolean;
|
|
154
162
|
} | {
|
|
155
163
|
kind: "account";
|
|
156
164
|
struct: StructDef;
|
|
@@ -175,9 +183,12 @@ export type ParamKind = {
|
|
|
175
183
|
kind: "token";
|
|
176
184
|
mut: boolean;
|
|
177
185
|
init?: "create" | "ifNeeded";
|
|
178
|
-
}
|
|
186
|
+
}
|
|
187
|
+
/** `init`: created in the body (`mint.create(payer, seeds | null, decimals, authority, freeze)`). */
|
|
188
|
+
| {
|
|
179
189
|
kind: "mint";
|
|
180
190
|
mut: boolean;
|
|
191
|
+
init?: "create" | "ifNeeded";
|
|
181
192
|
} | {
|
|
182
193
|
kind: "system";
|
|
183
194
|
} | {
|
|
@@ -260,6 +271,8 @@ export interface AccountParam {
|
|
|
260
271
|
optional?: boolean;
|
|
261
272
|
node: ts.ParameterDeclaration;
|
|
262
273
|
constraints: Constraints;
|
|
274
|
+
/** Created in the body as a new keypair account (`create(payer, null, …)`), so it signs: the IDL says so. */
|
|
275
|
+
signsInit?: boolean;
|
|
263
276
|
}
|
|
264
277
|
export interface ArgParam {
|
|
265
278
|
name: string;
|
|
@@ -287,6 +300,8 @@ export interface Instruction {
|
|
|
287
300
|
minArgsSize: number;
|
|
288
301
|
/** Sent as return data: a scalar in Borsh, or a fixed-size struct in its fixed layout (as the IDL describes it). */
|
|
289
302
|
returns: ScalarType | {
|
|
303
|
+
kind: "string";
|
|
304
|
+
} | {
|
|
290
305
|
kind: "struct";
|
|
291
306
|
name: string;
|
|
292
307
|
} | undefined;
|
|
@@ -302,6 +317,8 @@ export interface Instruction {
|
|
|
302
317
|
export interface GenericHelper {
|
|
303
318
|
name: string;
|
|
304
319
|
display: string;
|
|
320
|
+
/** Indexes of parameters with a function type (`f: (x: u64) => u64`): bound per call to a named function. */
|
|
321
|
+
fnParams: number[];
|
|
305
322
|
node: HelperNode;
|
|
306
323
|
typeParams: string[];
|
|
307
324
|
imports: Map<string, string>;
|
|
@@ -327,6 +344,8 @@ export interface Helper {
|
|
|
327
344
|
params: HelperParam[];
|
|
328
345
|
/** A struct result is written through an out pointer (`TSS_<name> *ret`) that the caller provides. */
|
|
329
346
|
returns: ScalarType | {
|
|
347
|
+
kind: "string";
|
|
348
|
+
} | {
|
|
330
349
|
kind: "struct";
|
|
331
350
|
name: string;
|
|
332
351
|
} | undefined;
|
|
@@ -335,6 +354,12 @@ export interface Helper {
|
|
|
335
354
|
statements: readonly ts.Statement[];
|
|
336
355
|
/** A generic helper's instance: type parameter -> the type it stands for. */
|
|
337
356
|
typeArgs?: Map<string, ScalarType>;
|
|
357
|
+
/** A generic helper's instance: type parameter -> the struct it stands for. */
|
|
358
|
+
structArgs?: Map<string, string>;
|
|
359
|
+
/** An instance's function parameters: name -> the helper passed for it (its calls go there). */
|
|
360
|
+
fnArgs?: Map<string, string>;
|
|
361
|
+
/** Their argument positions, which have no C parameter. */
|
|
362
|
+
fnArgIndexes?: number[];
|
|
338
363
|
/** The declaring file's `typesol` imports (local name -> export name). */
|
|
339
364
|
imports: Map<string, string>;
|
|
340
365
|
}
|
|
@@ -383,6 +408,8 @@ export interface ProgramModel {
|
|
|
383
408
|
generics: Map<string, GenericHelper>;
|
|
384
409
|
/** Declaration (function, or the const of an arrow) -> helper or generic name, for calls to local arrows. */
|
|
385
410
|
helperDecls: Map<ts.Node, string>;
|
|
411
|
+
/** Local closures (they read the enclosing function's variables): their declaration -> name; expanded where called. */
|
|
412
|
+
closureDecls: Map<ts.Node, string>;
|
|
386
413
|
errors: ErrorDef[];
|
|
387
414
|
/** Enum name -> member name -> error */
|
|
388
415
|
errorEnums: Map<string, Map<string, ErrorDef>>;
|