Skip to content
Draft
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
4 changes: 2 additions & 2 deletions packages/cashc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
"test": "test"
},
"scripts": {
"antlr": "antlr -Dlanguage=TypeScript -visitor -no-listener src/grammar/CashScript.g4",
"postantlr": "find src/grammar -type f -name 'CashScriptVisitor.ts' | xargs sed -i '' 's|\\(import .* \".*/.*\\)\";|\\1\\.js\";|g'",
"antlr": "antlr -Dlanguage=TypeScript -visitor -no-listener -o src/grammar src/grammar/CashScript.g4",
"postantlr": "node -e \"const fs=require('fs');['CashScriptVisitor.ts','CashScriptParser.ts','CashScriptLexer.ts'].forEach(f=>{const p='src/grammar/'+f;if(!fs.existsSync(p))return;const s=fs.readFileSync(p,'utf8').replace(/(from \\\"\\.[^\\\"]*?)(\\\")/g,(m,a,b)=>a.endsWith('.js')?m:a+'.js'+b);fs.writeFileSync(p,s);})\"",
"build": "yarn clean && yarn compile",
"clean": "rm -rf ./dist",
"compile": "tsc -p tsconfig.build.json",
Expand Down
42 changes: 42 additions & 0 deletions packages/cashc/src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ArrayNode,
TupleIndexOpNode,
RequireNode,
ReturnNode,
InstantiationNode,
StatementNode,
ContractNode,
Expand Down Expand Up @@ -115,6 +116,34 @@ export class EmptyFunctionError extends CashScriptError {
}
}

export class RecursiveFunctionError extends CashScriptError {
constructor(
public node: FunctionDefinitionNode,
public cycle: string[],
) {
super(node, `Recursive function call detected involving function '${node.name}' (cycle: ${cycle.join(' -> ')})`);
}
}

export class MissingReturnStatementError extends CashScriptError {
constructor(
public node: FunctionDefinitionNode,
) {
super(node, `Function '${node.name}' is missing a return statement on all paths`);
}
}

export class ReturnStatementError extends CashScriptError {
constructor(
// Also used for return-count / destructuring-arity mismatches, whose node may be the offending
// expression (e.g. the call being destructured) rather than a return statement.
public node: ReturnNode | FunctionDefinitionNode | ExpressionNode,
message: string,
) {
super(node, message);
}
}

export class FinalRequireStatementError extends CashScriptError {
constructor(
public node: StatementNode,
Expand All @@ -134,6 +163,19 @@ export class TypeError extends CashScriptError {
}
}

export class ReturnTypeError extends TypeError {
constructor(
node: ReturnNode,
actual: Type | undefined,
expected: Type | undefined,
) {
super(
node, actual, expected,
`Found return value of type '${actual}' where type '${expected}' was expected`,
);
}
}

export class InvalidParameterTypeError extends TypeError {
constructor(
node: FunctionCallNode | RequireNode | InstantiationNode,
Expand Down
18 changes: 11 additions & 7 deletions packages/cashc/src/artifact/Artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ export function generateArtifact(
const constructorInputs = contract.parameters
.map((parameter) => ({ name: parameter.name, type: parameter.type.toString() }));

const abi = contract.functions.map((func) => ({
name: func.name,
inputs: func.parameters.map((parameter) => ({
name: parameter.name,
type: parameter.type.toString(),
})),
}));
// User-defined (value-returning) functions are internal helpers compiled to OP_DEFINE/OP_INVOKE;
// they are not spending paths and so are excluded from the ABI.
const abi = contract.functions
.filter((func) => !func.isUserFunction)
.map((func) => ({
name: func.name,
inputs: func.parameters.map((parameter) => ({
name: parameter.name,
type: parameter.type.toString(),
})),
}));

const bytecode = scriptToAsm(script);

Expand Down
38 changes: 35 additions & 3 deletions packages/cashc/src/ast/AST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,21 @@ export class FunctionDefinitionNode extends Node implements Named {
public name: string,
public parameters: ParameterNode[],
public body: BlockNode,
// User-defined functions (declared with `returns (...)`) return one or more values and are
// compiled to standalone OP_DEFINE/OP_INVOKE routines; contract spending functions have no
// declared return types and are compiled directly. A single-return function has one element,
// a multi-return (tuple) function has N elements in declared order.
public returnTypes?: Type[],
) {
super();
}

// A user-defined (reusable) function is one with a declared return type list. These are lowered
// to OP_DEFINE/OP_INVOKE and are not emitted as standalone spending functions.
get isUserFunction(): boolean {
return this.returnTypes !== undefined;
}

accept<T>(visitor: AstVisitor<T>): T {
return visitor.visitFunctionDefinition(this);
}
Expand Down Expand Up @@ -98,11 +109,18 @@ export class VariableDefinitionNode extends NonControlStatementNode implements N
}
}

export interface TupleTarget {
name: string;
type: Type;
}

export class TupleAssignmentNode extends NonControlStatementNode {
constructor(
// TODO: Use an IdentifierNode instead of a custom type
public left: { name: string, type: Type },
public right: { name: string, type: Type },
// TODO: Use IdentifierNodes instead of a custom type
// A tuple assignment destructures a tuple-valued expression into N >= 2 named targets in order.
// The 2-target form is used for built-in multi-value expressions (e.g. `.split`); N-target forms
// (N >= 2) destructure the result of a multi-return user-defined function call.
public targets: TupleTarget[],
public tuple: ExpressionNode,
) {
super();
Expand Down Expand Up @@ -156,6 +174,20 @@ export class RequireNode extends NonControlStatementNode {
}
}

export class ReturnNode extends NonControlStatementNode {
constructor(
// A return statement yields one or more values in declared order (one for a single-return
// function, N for a multi-return/tuple function).
public expressions: ExpressionNode[],
) {
super();
}

accept<T>(visitor: AstVisitor<T>): T {
return visitor.visitReturn(this);
}
}

export class ConsoleStatementNode extends NonControlStatementNode {
constructor(
public parameters: ConsoleParameterNode[],
Expand Down
21 changes: 18 additions & 3 deletions packages/cashc/src/ast/AstBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
ArrayNode,
TupleIndexOpNode,
RequireNode,
ReturnNode,
InstantiationNode,
TupleAssignmentNode,
NullaryOpNode,
Expand Down Expand Up @@ -62,6 +63,7 @@ import type {
LiteralExpressionContext,
TupleIndexOpContext,
RequireStatementContext,
ReturnStatementContext,
PragmaDirectiveContext,
InstantiationContext,
NullaryOpContext,
Expand Down Expand Up @@ -145,7 +147,13 @@ export default class AstBuilder
const name = ctx.Identifier().getText();
const parameters = ctx.parameterList().parameter_list().map((p) => this.visit(p) as ParameterNode);
const body = this.visit(ctx.functionBody());
const functionDefinition = new FunctionDefinitionNode(name, parameters, body);
// The optional `returns (type, ...)` clause distinguishes user-defined functions from contract
// spending functions, which have no return types. A user function returns one or more values.
const returnTypeContexts = ctx.typeName_list();
const returnTypes = returnTypeContexts.length > 0
? returnTypeContexts.map((t) => parseType(t.getText()))
: undefined;
const functionDefinition = new FunctionDefinitionNode(name, parameters, body, returnTypes);
functionDefinition.location = Location.fromCtx(ctx);
return functionDefinition;
}
Expand Down Expand Up @@ -199,11 +207,11 @@ export default class AstBuilder
const expression = this.visit(ctx.expression());
const names = ctx.Identifier_list();
const types = ctx.typeName_list();
const [var1, var2] = names.map((name, i) => ({
const targets = names.map((name, i) => ({
name: name.getText(),
type: parseType(types[i].getText()),
}));
const tupleAssignment = new TupleAssignmentNode(var1, var2, expression);
const tupleAssignment = new TupleAssignmentNode(targets, expression);
tupleAssignment.location = Location.fromCtx(ctx);
return tupleAssignment;
}
Expand Down Expand Up @@ -260,6 +268,13 @@ export default class AstBuilder
return require;
}

visitReturnStatement(ctx: ReturnStatementContext): ReturnNode {
const expressions = ctx.expression_list().map((e) => this.visit(e) as ExpressionNode);
const returnNode = new ReturnNode(expressions);
returnNode.location = Location.fromCtx(ctx);
return returnNode;
}

visitIfStatement(ctx: IfStatementContext): BranchNode {
const condition = this.visit(ctx.expression());
const ifBlock = this.visit(ctx._ifBlock) as StatementNode;
Expand Down
6 changes: 6 additions & 0 deletions packages/cashc/src/ast/AstTraversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ArrayNode,
TupleIndexOpNode,
RequireNode,
ReturnNode,
InstantiationNode,
TupleAssignmentNode,
NullaryOpNode,
Expand Down Expand Up @@ -82,6 +83,11 @@ export default class AstTraversal extends AstVisitor<Node> {
return node;
}

visitReturn(node: ReturnNode): Node {
node.expressions = this.visitList(node.expressions);
return node;
}

visitConsoleStatement(node: ConsoleStatementNode): Node {
node.parameters = this.visitList(node.parameters) as ConsoleParameterNode[];
return node;
Expand Down
2 changes: 2 additions & 0 deletions packages/cashc/src/ast/AstVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
ArrayNode,
TupleIndexOpNode,
RequireNode,
ReturnNode,
InstantiationNode,
TupleAssignmentNode,
NullaryOpNode,
Expand All @@ -41,6 +42,7 @@ export default abstract class AstVisitor<T> {
abstract visitAssign(node: AssignNode): T;
abstract visitTimeOp(node: TimeOpNode): T;
abstract visitRequire(node: RequireNode): T;
abstract visitReturn(node: ReturnNode): T;
abstract visitConsoleStatement(node: ConsoleStatementNode): T;
abstract visitBranch(node: BranchNode): T;
abstract visitDoWhile(node: DoWhileNode): T;
Expand Down
11 changes: 9 additions & 2 deletions packages/cashc/src/ast/SymbolTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export class Symbol {
public symbolType: SymbolType,
public definition?: Node,
public parameters?: Type[],
// For user-defined functions: the full ordered list of declared return types. A single-return
// function has one element (matching `type`); a multi-return function has N elements. Used to
// type-check and bind tuple-destructuring call sites.
public returnTypes?: Type[],
) {}

static variable(node: VariableDefinitionNode | ParameterNode): Symbol {
Expand All @@ -24,8 +28,8 @@ export class Symbol {
return new Symbol(name, type, SymbolType.VARIABLE);
}

static function(name: string, type: Type, parameters: Type[]): Symbol {
return new Symbol(name, type, SymbolType.FUNCTION, undefined, parameters);
static function(name: string, type: Type, parameters: Type[], definition?: Node, returnTypes?: Type[]): Symbol {
return new Symbol(name, type, SymbolType.FUNCTION, definition, parameters, returnTypes);
}

static class(name: string, type: Type, parameters: Type[]): Symbol {
Expand Down Expand Up @@ -73,6 +77,9 @@ export class SymbolTable {
unusedSymbols(): Symbol[] {
return Array.from(this.symbols)
.map((e) => e[1])
// Only variables are subject to the unused-variable check; user-defined function symbols
// may legitimately go uncalled (and are reported elsewhere if needed).
.filter((s) => s.symbolType === SymbolType.VARIABLE)
.filter((s) => s.references.length === 0);
}
}
Loading
Loading