Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/cashc/src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,26 @@ export class AssignTypeError extends TypeError {
}
}

export class InvalidConstantExpressionError extends CashScriptError {
constructor(
public node: Node,
) {
super(
node,
'Global constant definitions only support literals, references to other constants, '
+ 'integer arithmetic and concatenation',
);
}
}

export class DivisionByZeroError extends CashScriptError {
constructor(
public node: BinaryOpNode,
) {
super(node, 'Division by zero');
}
}

export class ConstantModificationError extends CashScriptError {
constructor(node: VariableDefinitionNode | ConstantDefinitionNode);
constructor(node: Node, name: string);
Expand Down
2 changes: 1 addition & 1 deletion packages/cashc/src/ast/AST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class ConstantDefinitionNode extends Node implements Named, Typed {
constructor(
public type: Type,
public name: string,
public value: LiteralNode,
public value: ExpressionNode,
) {
super();
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cashc/src/ast/AstBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export default class AstBuilder
visitConstantDefinition(ctx: ConstantDefinitionContext): ConstantDefinitionNode {
const type = parseType(ctx.typeName().getText());
const name = ctx.Identifier().getText();
const value = this.createLiteral(ctx.literal());
const value = this.visit(ctx.expression()) as ExpressionNode;
const constantDefinition = new ConstantDefinitionNode(type, name, value);
constantDefinition.location = Location.fromCtx(ctx);
return constantDefinition;
Expand Down
4 changes: 2 additions & 2 deletions packages/cashc/src/ast/AstTraversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
NullaryOpNode,
ConsoleStatementNode,
ConsoleParameterNode,
LiteralNode,
ExpressionNode,
FunctionCallStatementNode,
SliceNode,
DoWhileNode,
Expand Down Expand Up @@ -64,7 +64,7 @@ export default class AstTraversal extends AstVisitor<Node> {
}

visitConstantDefinition(node: ConstantDefinitionNode): Node {
node.value = this.visit(node.value) as LiteralNode;
node.value = this.visit(node.value) as ExpressionNode;
return node;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/cashc/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
resolveDependencies,
} from './dependency-resolution.js';
import GenerateTargetTraversal from './generation/GenerateTargetTraversal.js';
import { FoldGlobalConstantsTraversal } from './semantic/FoldGlobalConstantsTraversal.js';
import SymbolTableTraversal from './semantic/SymbolTableTraversal.js';
import TypeCheckTraversal from './semantic/TypeCheckTraversal.js';
import EnsureFinalRequireTraversal from './semantic/EnsureFinalRequireTraversal.js';
Expand Down Expand Up @@ -115,6 +116,7 @@ function compileCode(
const constructorParamLength = ast.contract.parameters.length;

// Semantic analysis
ast = ast.accept(new FoldGlobalConstantsTraversal()) as Ast;
ast = ast.accept(new SymbolTableTraversal()) as Ast;
ast = ast.accept(new TypeCheckTraversal()) as Ast;
ast = ast.accept(new EnsureFunctionsSafeTraversal()) as Ast;
Expand Down
15 changes: 11 additions & 4 deletions packages/cashc/src/dependency-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,22 @@ interface ImportedDefinitions {
}

// Depth-first walk of the import graph, returning every global definition it reaches. `visitedPaths`
// is internal bookkeeping that de-duplicates files by canonical path — collapsing diamonds (a file
// reached through two paths is read once) and guaranteeing termination for mutual or cyclic imports —
// so this function stays pure with respect to its arguments.
// de-duplicates files by canonical path so a diamond's shared leaf is read once, while `activePaths`
// tracks the files currently being resolved so cyclic imports are rejected.
function collectImports(
imports: ImportNode[],
resolver: ImportResolver,
errorListener?: CashScriptErrorListener,
): ImportedDefinitions {
const visitedPaths = new Set<string>();
const activePaths = new Set<string>();

const collect = (currentImports: ImportNode[], currentDir: string): ImportedDefinitions[] =>
currentImports.flatMap((importNode) => {
const canonicalPath = resolver.resolve(currentDir, importNode.path);
if (activePaths.has(canonicalPath)) {
throw new ImportResolutionError(importNode, `Cyclic import of '${importNode.path}'`);
}
if (visitedPaths.has(canonicalPath)) return [];
visitedPaths.add(canonicalPath);

Expand All @@ -118,8 +121,12 @@ function collectImports(
constant.sourceFile = resolver.sourceName(canonicalPath);
});

activePaths.add(canonicalPath);
const transitiveDefinitions = collect(importedAst.imports, resolver.dirname(canonicalPath));
activePaths.delete(canonicalPath);

return [
...collect(importedAst.imports, resolver.dirname(canonicalPath)),
...transitiveDefinitions,
{ functions: importedAst.functions, constants: importedAst.constants },
];
});
Expand Down
2 changes: 1 addition & 1 deletion packages/cashc/src/grammar/CashScript.g4
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ globalFunctionDefinition
;

constantDefinition
: typeName 'constant' Identifier '=' literal ';'
: typeName 'constant' Identifier '=' expression ';'
;

contractDefinition
Expand Down
2 changes: 1 addition & 1 deletion packages/cashc/src/grammar/CashScript.interp

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions packages/cashc/src/grammar/CashScriptParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ export default class CashScriptParser extends Parser {
this.state = 157;
this.match(CashScriptParser.T__9);
this.state = 158;
this.literal();
this.expression(0);
this.state = 159;
this.match(CashScriptParser.T__1);
}
Expand Down Expand Up @@ -2812,7 +2812,7 @@ export default class CashScriptParser extends Parser {
0,146,148,1,0,0,0,147,145,1,0,0,0,148,149,5,16,0,0,149,151,1,0,0,0,150,
138,1,0,0,0,150,151,1,0,0,0,151,152,1,0,0,0,152,153,3,24,12,0,153,17,1,
0,0,0,154,155,3,84,42,0,155,156,5,17,0,0,156,157,5,82,0,0,157,158,5,10,
0,0,158,159,3,80,40,0,159,160,5,2,0,0,160,19,1,0,0,0,161,162,5,18,0,0,162,
0,0,158,159,3,76,38,0,159,160,5,2,0,0,160,19,1,0,0,0,161,162,5,18,0,0,162,
163,5,82,0,0,163,164,3,26,13,0,164,168,5,19,0,0,165,167,3,22,11,0,166,165,
1,0,0,0,167,170,1,0,0,0,168,166,1,0,0,0,168,169,1,0,0,0,169,171,1,0,0,0,
170,168,1,0,0,0,171,172,5,20,0,0,172,21,1,0,0,0,173,174,5,12,0,0,174,175,
Expand Down Expand Up @@ -3176,8 +3176,8 @@ export class ConstantDefinitionContext extends ParserRuleContext {
public Identifier(): TerminalNode {
return this.getToken(CashScriptParser.Identifier, 0);
}
public literal(): LiteralContext {
return this.getTypedRuleContext(LiteralContext, 0) as LiteralContext;
public expression(): ExpressionContext {
return this.getTypedRuleContext(ExpressionContext, 0) as ExpressionContext;
}
public get ruleIndex(): number {
return CashScriptParser.RULE_constantDefinition;
Expand Down
3 changes: 1 addition & 2 deletions packages/cashc/src/print/OutputSourceCodeTraversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import {
ForNode,
NonControlStatementNode,
ExpressionNode,
LiteralNode,
} from '../ast/AST.js';
import AstTraversal from '../ast/AstTraversal.js';

Expand Down Expand Up @@ -112,7 +111,7 @@ export default class OutputSourceCodeTraversal extends AstTraversal {

visitConstantDefinition(node: ConstantDefinitionNode): Node {
this.addOutput(`${node.type} constant ${node.name} = `, true);
node.value = this.visit(node.value) as LiteralNode;
node.value = this.visit(node.value) as ExpressionNode;
this.addOutput(';\n');
return node;
}
Expand Down
149 changes: 149 additions & 0 deletions packages/cashc/src/semantic/FoldGlobalConstantsTraversal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { PrimitiveType } from '@cashscript/utils';
import {
BinaryOpNode,
ConstantDefinitionNode,
ExpressionNode,
HexLiteralNode,
IdentifierNode,
IntLiteralNode,
LiteralNode,
Node,
SourceFileNode,
StringLiteralNode,
UnaryOpNode,
} from '../ast/AST.js';
import AstTraversal from '../ast/AstTraversal.js';
import { GLOBAL_SYMBOL_TABLE } from '../ast/Globals.js';
import { BinaryOperator, UnaryOperator } from '../ast/Operator.js';
import { cloneConstantValue } from './LowerGlobalConstantsTraversal.js';
import { resultingTypeForBinaryOp } from '../utils.js';
import {
CashScriptError,
DivisionByZeroError,
InvalidConstantExpressionError,
UndefinedReferenceError,
UnequalTypeError,
UnsupportedTypeError,
} from '../Errors.js';

// Supports literals, references to other constants, integer arithmetic (+, -, *, /, %, unary -) and concatenation (+)
export class FoldGlobalConstantsTraversal extends AstTraversal {
private foldedConstants: Map<string, ConstantDefinitionNode> = new Map();
private functionNames: Set<string> = new Set();

visitSourceFile(node: SourceFileNode): Node {
this.functionNames = new Set(node.functions.map((func) => func.name));
node.constants = this.visitList(node.constants) as ConstantDefinitionNode[];
return node;
}

visitConstantDefinition(node: ConstantDefinitionNode): Node {
node.value = this.visitExpression(node.value);
this.foldedConstants.set(node.name, node);
return node;
}

// Folds an expression through the regular visitor dispatch and rejects any expression kind
// that did not fold down to a single literal
private visitExpression(node: ExpressionNode): LiteralNode {
const folded = this.visit(node);
if (!(folded instanceof LiteralNode)) throw new InvalidConstantExpressionError(node);
return folded;
}

visitIdentifier(node: IdentifierNode): Node {
const constant = this.foldedConstants.get(node.name);
if (constant) return cloneConstantValue(constant, node);

// Existing names (except previously declared constants) are invalid in a constant initialiser
if (this.functionNames.has(node.name) || GLOBAL_SYMBOL_TABLE.get(node.name)) {
throw new InvalidConstantExpressionError(node);
}

throw new UndefinedReferenceError(node);
}

visitUnaryOp(node: UnaryOpNode): Node {
if (!FOLDABLE_UNARY_OPERATORS.includes(node.operator)) throw new InvalidConstantExpressionError(node);

node.expression = this.visitExpression(node.expression);
if (!(node.expression instanceof IntLiteralNode)) {
throw new UnsupportedTypeError(node, node.expression.type, PrimitiveType.INT);
}

return withLocation(new IntLiteralNode(-node.expression.value), node);
}

visitBinaryOp(node: BinaryOpNode): Node {
if (!FOLDABLE_BINARY_OPERATORS.includes(node.operator)) throw new InvalidConstantExpressionError(node);

// The folded operands are written back so the type errors below report the resolved operand types
node.left = this.visitExpression(node.left);
node.right = this.visitExpression(node.right);

if (node.operator === BinaryOperator.PLUS) return foldPlus(node);
return foldIntArithmetic(node);
}
}

const FOLDABLE_UNARY_OPERATORS = [
UnaryOperator.NEGATE,
];

const FOLDABLE_BINARY_OPERATORS = [
BinaryOperator.PLUS,
BinaryOperator.MINUS,
BinaryOperator.MUL,
BinaryOperator.DIV,
BinaryOperator.MOD,
];

function foldPlus(node: BinaryOpNode): LiteralNode {
const { left, right } = node;

if (left instanceof IntLiteralNode && right instanceof IntLiteralNode) {
return withLocation(new IntLiteralNode(left.value + right.value), node);
}

if (left instanceof StringLiteralNode && right instanceof StringLiteralNode) {
return withLocation(new StringLiteralNode(left.value + right.value, left.quote), node);
}

if (left instanceof HexLiteralNode && right instanceof HexLiteralNode) {
return withLocation(new HexLiteralNode(new Uint8Array([...left.value, ...right.value])), node);
}

throw typeMismatchError(node, PrimitiveType.INT);
}

function foldIntArithmetic(node: BinaryOpNode): LiteralNode {
const { left, right, operator } = node;

if (!(left instanceof IntLiteralNode) || !(right instanceof IntLiteralNode)) {
throw typeMismatchError(node, PrimitiveType.INT);
}

if ((operator === BinaryOperator.DIV || operator === BinaryOperator.MOD) && right.value === 0n) {
throw new DivisionByZeroError(node);
}

switch (operator) {
case BinaryOperator.MINUS: return withLocation(new IntLiteralNode(left.value - right.value), node);
case BinaryOperator.MUL: return withLocation(new IntLiteralNode(left.value * right.value), node);
// Note: BigInt division and modulo truncate towards zero, matching OP_DIV / OP_MOD semantics
case BinaryOperator.DIV: return withLocation(new IntLiteralNode(left.value / right.value), node);
case BinaryOperator.MOD: return withLocation(new IntLiteralNode(left.value % right.value), node);
default: throw new InvalidConstantExpressionError(node);
}
}

function typeMismatchError(node: BinaryOpNode, expected: PrimitiveType): CashScriptError {
const resultingType = resultingTypeForBinaryOp(node.operator, node.left.type!, node.right.type!);
if (resultingType) return new UnsupportedTypeError(node, resultingType, expected);
return new UnequalTypeError(node);
}

function withLocation<T extends LiteralNode>(literal: T, source: Node): T {
literal.location = source.location;
return literal;
}
17 changes: 13 additions & 4 deletions packages/cashc/src/semantic/LowerGlobalConstantsTraversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ConsoleStatementNode,
ConstantDefinitionNode,
ContractNode,
ExpressionNode,
FunctionCallNode,
FunctionDefinitionNode,
FunctionKind,
Expand Down Expand Up @@ -96,17 +97,25 @@ function createConstantFunction(constant: ConstantDefinitionNode): FunctionDefin
return definition;
}

// Create a synthetic LiteralNode that represents a reference to a lowered constant function,
// so later passes can treat it as a literal
export function createConstantLiteral(constant: ConstantDefinitionNode, reference: IdentifierNode): LiteralNode {
// Clone a constant's literal value, adopting the reference's location and the constant's declared type
export function cloneConstantValue(constant: ConstantDefinitionNode, reference: IdentifierNode): LiteralNode {
const literal = cloneLiteral(constant.value);
literal.location = reference.location;
literal.type = constant.type;
return literal;
}

// Create a synthetic LiteralNode that represents a reference to a lowered constant function,
// so later passes can treat it as a literal
export function createConstantLiteral(constant: ConstantDefinitionNode, reference: IdentifierNode): LiteralNode {
const literal = cloneConstantValue(constant, reference);
literal.constant = constant;
return literal;
}

function cloneLiteral(node: LiteralNode): LiteralNode {
// SymbolTableTraversal folds every constant's value to a literal before constants are lowered or referenced
function cloneLiteral(node: ExpressionNode): LiteralNode {
if (!(node instanceof LiteralNode)) throw new Error('Expected constant value to be folded to a literal'); // Shouldn't happen
const clone: LiteralNode = Object.assign(Object.create(Object.getPrototypeOf(node)), node);
if (clone instanceof HexLiteralNode) clone.value = clone.value.slice();
return clone;
Expand Down
1 change: 1 addition & 0 deletions packages/cashc/src/semantic/TypeCheckTraversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default class TypeCheckTraversal extends AstTraversal {
private currentFunctionReturnTypes: Type[] = [];

visitConstantDefinition(node: ConstantDefinitionNode): Node {
// The constant's value has already been folded to a literal by SymbolTableTraversal
node.value = this.visit(node.value) as LiteralNode;
expectAssignable(node, node.value.type, node.type);
return node;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
int constant DIVIDED = 10 / 0;

contract GlobalConstantDivisionByZero() {
function spend() {
require(DIVIDED == 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
int constant LOCKTIME = tx.locktime;

contract GlobalConstantUnsupportedExpression() {
function spend() {
require(LOCKTIME == 0);
}
}

This file was deleted.

Loading
Loading