Skip to content

Commit c362875

Browse files
committed
Add extra type inference and type checking for SPLIT
- If split() is called with a number literal (e.g.) x.split(12), we infer one or both sides of the split - Extend type checking to use this new type information - Add tests
1 parent 70cd502 commit c362875

15 files changed

+118
-29
lines changed

packages/cashc/src/Errors.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import {
2020
ContractNode,
2121
ExpressionNode,
2222
SliceNode,
23+
IntLiteralNode,
2324
} from './ast/AST.js';
2425
import { Symbol, SymbolType } from './ast/SymbolTable.js';
2526
import { Location, Point } from './ast/Location.js';
27+
import { BinaryOperator } from './ast/Operator.js';
2628

2729
export class CashScriptError extends Error {
2830
node: Node;
@@ -252,9 +254,16 @@ export class ArrayElementError extends CashScriptError {
252254

253255
export class IndexOutOfBoundsError extends CashScriptError {
254256
constructor(
255-
node: TupleIndexOpNode,
257+
node: TupleIndexOpNode | BinaryOpNode,
256258
) {
257-
super(node, `Index ${node.index} out of bounds`);
259+
if (node instanceof TupleIndexOpNode) {
260+
super(node, `Index ${node.index} out of bounds`);
261+
} else if (node.operator === BinaryOperator.SPLIT && node.right instanceof IntLiteralNode) {
262+
const splitIndex = Number(node.right.value);
263+
super(node, `Split index ${splitIndex} out of bounds for type ${node.left.type}`);
264+
} else {
265+
super(node, 'Index out of bounds');
266+
}
258267
}
259268
}
260269

packages/cashc/src/ast/AST.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ export class VariableDefinitionNode extends StatementNode implements Named, Type
9898

9999
export class TupleAssignmentNode extends StatementNode {
100100
constructor(
101-
public var1: { name: string, type: Type },
102-
public var2: { name: string, type: Type },
101+
// TODO: Use an IdentifierNode instead of a custom type
102+
public left: { name: string, type: Type },
103+
public right: { name: string, type: Type },
103104
public tuple: ExpressionNode,
104105
) {
105106
super();

packages/cashc/src/generation/GenerateTargetTraversal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,8 @@ export default class GenerateTargetTraversalWithLocation extends AstTraversal {
252252
visitTupleAssignment(node: TupleAssignmentNode): Node {
253253
node.tuple = this.visit(node.tuple);
254254
this.popFromStack(2);
255-
this.pushToStack(node.var1.name);
256-
this.pushToStack(node.var2.name);
255+
this.pushToStack(node.left.name);
256+
this.pushToStack(node.right.name);
257257
return node;
258258
}
259259

packages/cashc/src/print/OutputSourceCodeTraversal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export default class OutputSourceCodeTraversal extends AstTraversal {
110110
}
111111

112112
visitTupleAssignment(node: TupleAssignmentNode): Node {
113-
this.addOutput(`${node.var1.type} ${node.var1.name}, ${node.var2.type} ${node.var2.name} = `, true);
113+
this.addOutput(`${node.left.type} ${node.left.name}, ${node.right.type} ${node.right.name} = `, true);
114114
this.visit(node.tuple);
115115
this.addOutput(';\n');
116116

packages/cashc/src/semantic/SymbolTableTraversal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export default class SymbolTableTraversal extends AstTraversal {
124124
}
125125

126126
visitTupleAssignment(node: TupleAssignmentNode): Node {
127-
[node.var1, node.var2].forEach(({ name, type }) => {
127+
[node.left, node.right].forEach(({ name, type }) => {
128128
if (this.symbolTables[0].get(name)) {
129129
throw new VariableRedefinitionError(new VariableDefinitionNode(type, [], name, node.tuple));
130130
}

packages/cashc/src/semantic/TypeCheckTraversal.ts

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
TupleAssignmentNode,
2929
NullaryOpNode,
3030
SliceNode,
31+
IntLiteralNode,
3132
} from '../ast/AST.js';
3233
import AstTraversal from '../ast/AstTraversal.js';
3334
import {
@@ -57,17 +58,13 @@ export default class TypeCheckTraversal extends AstTraversal {
5758
if (!(node.tuple instanceof BinaryOpNode) || node.tuple.operator !== BinaryOperator.SPLIT) {
5859
throw new TupleAssignmentError(node.tuple);
5960
}
60-
const tupleType = node.tuple.left.type;
61-
for (const variable of [node.var1, node.var2]) {
62-
if (!implicitlyCastable(tupleType, variable.type)) {
63-
// Ignore if both are of type byte. problem: bytes16 can be typed to bytes32
64-
if (tupleType instanceof BytesType && variable.type instanceof BytesType) {
65-
return node;
66-
}
67-
throw new AssignTypeError(
68-
new VariableDefinitionNode(variable.type, [], variable.name, node.tuple),
69-
);
70-
}
61+
62+
const assignmentType = new TupleType(node.left.type, node.right.type);
63+
64+
if (!implicitlyCastable(node.tuple.type, assignmentType)) {
65+
throw new AssignTypeError(
66+
new VariableDefinitionNode(assignmentType, [], node.left.name, node.tuple),
67+
);
7168
}
7269
return node;
7370
}
@@ -240,12 +237,7 @@ export default class TypeCheckTraversal extends AstTraversal {
240237
case BinaryOperator.SPLIT:
241238
expectAnyOfTypes(node, node.left.type, [new BytesType(), PrimitiveType.STRING]);
242239
expectInt(node, node.right.type);
243-
244-
// Result of split are two unbounded bytes types (could be improved to do type inference)
245-
node.type = new TupleType(
246-
node.left.type instanceof BytesType ? new BytesType() : PrimitiveType.STRING,
247-
node.left.type instanceof BytesType ? new BytesType() : PrimitiveType.STRING,
248-
);
240+
node.type = inferTupleType(node);
249241
return node;
250242
default:
251243
return node;
@@ -346,9 +338,7 @@ export default class TypeCheckTraversal extends AstTraversal {
346338
type ExpectedNode = BinaryOpNode | UnaryOpNode | TimeOpNode | TupleIndexOpNode | SliceNode;
347339
function expectAnyOfTypes(node: ExpectedNode, actual?: Type, expectedTypes?: Type[]): void {
348340
if (!expectedTypes || expectedTypes.length === 0) return;
349-
if (expectedTypes.find((expected) => implicitlyCastable(actual, expected))) {
350-
return;
351-
}
341+
if (expectedTypes.find((expected) => implicitlyCastable(actual, expected))) return;
352342

353343
throw new UnsupportedTypeError(node, actual, expectedTypes[0]);
354344
}
@@ -392,3 +382,37 @@ function expectParameters(node: NodeWithParameters, actual: Type[], expected: Ty
392382
throw new InvalidParameterTypeError(node, actual, expected);
393383
}
394384
}
385+
386+
// We only call this function for the split operator, so we assume that the node.op is SPLIT
387+
function inferTupleType(node: BinaryOpNode): Type {
388+
// string.split() -> string, string
389+
if (node.left.type === PrimitiveType.STRING) {
390+
return new TupleType(PrimitiveType.STRING, PrimitiveType.STRING);
391+
}
392+
393+
// If the expression is not a bytes type, then it must be a different compatible type (e.g. sig/pubkey)
394+
// We treat this as an unbounded bytes type for the purposes of splitting
395+
const expressionType = node.left.type instanceof BytesType ? node.left.type : new BytesType();
396+
397+
// bytes.split(variable) -> bytes, bytes
398+
if (!(node.right instanceof IntLiteralNode)) {
399+
return new TupleType(new BytesType(), new BytesType());
400+
}
401+
402+
const splitIndex = Number(node.right.value);
403+
404+
// bytes.split(NumberLiteral) -> bytes(NumberLiteral), bytes
405+
if (expressionType.bound === undefined) {
406+
return new TupleType(new BytesType(splitIndex), new BytesType());
407+
}
408+
409+
if (splitIndex > expressionType.bound) {
410+
throw new IndexOutOfBoundsError(node);
411+
}
412+
413+
// bytesX.split(NumberLiteral) -> bytes(NumberLiteral), bytes(X - NumberLiteral)
414+
return new TupleType(
415+
new BytesType(splitIndex),
416+
new BytesType(expressionType.bound! - splitIndex),
417+
);
418+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
contract Test(bytes b) {
2+
function spend() {
3+
bytes2 x = b.split(4)[0];
4+
require(x != b);
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
contract Test(bytes8 b) {
2+
function spend() {
3+
bytes2 x = b.split(4)[1];
4+
require(x != b);
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
contract Test(bytes b) {
2+
function spend() {
3+
bytes4 x, bytes4 y = b.split(4);
4+
require(x != y);
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
contract Test(bytes8 b) {
2+
function spend() {
3+
bytes2 x, bytes4 y = b.split(4);
4+
require(x != y);
5+
}
6+
}

0 commit comments

Comments
 (0)