@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.
@@ -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) || ts.isForInStatement(stmt))
69
- this.fail(stmt, "only for (init; cond; step), for (const x of list) and while loops are supported");
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.line(`sol_set_return_data((const uint8_t *)&${t}, sizeof(${t}));`);
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
- if (typed.type.kind === "int" && INT_INFO[typed.type.name].bytes === 16)
150
- this.fail(stmt.expression, "switch on 128-bit integers is not supported; use if/else");
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.line(`switch (${t}) {`);
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
- this.line(`case ${label.c}: {`);
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 && ts.isTypeReferenceNode(d.type) && ts.isIdentifier(d.type.typeName) && !d.type.typeArguments
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
- this.fail(e.operatorToken, "write x = x ** n instead of **=");
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: only instructions know it.
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) && !!this.ix,
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
- const src = this.ref(valueNode);
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.
@@ -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;
@@ -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>>;