@typesol/cli 0.17.0 → 0.19.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)) {
@@ -489,7 +842,24 @@ export class StatementEmitter extends CpiEmitter {
489
842
  this.line(`TS_Str ${cur};`);
490
843
  this.line(`TS_TRY(ts_str_copy(${current.c}, &${cur}));`);
491
844
  }
845
+ // A local grows in place when it is the newest heap string (a loop of `s += …` copies each piece once); a
846
+ // template on the right is appended piece by piece.
847
+ const local = cur === current.c && place.write("X") === `${cur} = X`;
848
+ const bare = (e) => (ts.isParenthesizedExpression(e) ? bare(e.expression) : e);
849
+ const rhsNode = bare(valueNode);
850
+ if (local && ts.isTemplateExpression(rhsNode)) {
851
+ const pieces = this.templatePieces(rhsNode);
852
+ if (pieces.text !== undefined)
853
+ this.line(`TS_TRY(ts_str_append(&${cur}, ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})));`);
854
+ else
855
+ this.line(`TS_TRY(ts_str_append_parts(&${cur}, ${pieces.arr}, ${pieces.n}));`);
856
+ return;
857
+ }
492
858
  const rhs = this.toText(valueNode);
859
+ if (local) {
860
+ this.line(`TS_TRY(ts_str_append(&${cur}, ${rhs}));`);
861
+ return;
862
+ }
493
863
  const t = this.tmp("cat");
494
864
  this.line(`TS_Str ${t};`);
495
865
  this.line(`TS_TRY(ts_str_concat(${cur}, ${rhs}, &${t}));`);
@@ -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;