Skip to content
Open
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
10 changes: 1 addition & 9 deletions packages/cashc/src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
IntLiteralNode,
TupleAssignmentNode,
} from './ast/AST.js';
import { Symbol, SymbolType } from './ast/SymbolTable.js';
import { SymbolType } from './ast/SymbolTable.js';
import { Location } from './ast/Location.js';
import { BinaryOperator } from './ast/Operator.js';

Expand Down Expand Up @@ -98,14 +98,6 @@ export class ImportResolutionError extends CashScriptError {
}
}

export class UnusedVariableError extends CashScriptError {
constructor(
public symbol: Symbol,
) {
super(symbol.definition as Node, `Unused variable ${symbol.name}`);
}
}

export class EmptyContractError extends CashScriptError {
constructor(
public node: ContractNode,
Expand Down
41 changes: 41 additions & 0 deletions packages/cashc/src/Warnings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { IdentifierNode, Node } from './ast/AST.js';
import { Symbol } from './ast/SymbolTable.js';

export class CashScriptWarning {
name: string;
message: string;

constructor(
public node: Node,
message: string,
) {
if (node.location) {
message += ` at ${node.location.start}`;
}

this.name = this.constructor.name;
this.message = message;
}
}

export class UnusedVariableWarning extends CashScriptWarning {
constructor(
public symbol: Symbol,
) {
super(symbol.definition as Node, `Unused variable '${symbol.name}'`);
}
}

export class UnusedAssignmentWarning extends CashScriptWarning {
constructor(
public identifier: IdentifierNode,
) {
super(identifier, `Value assigned to '${identifier.name}' is never read`);
}
}

export type CashScriptWarningListener = (warning: CashScriptWarning) => void;

export const defaultWarningListener: CashScriptWarningListener = (warning) => {
console.warn(`Warning: ${warning.message}`);
};
4 changes: 4 additions & 0 deletions packages/cashc/src/ast/AST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export class FunctionDefinitionNode extends Node implements Named {
}

export class ParameterNode extends Node implements Named, Typed {
symbol?: Symbol;

constructor(
public type: Type,
public modifiers: Modifier[],
Expand All @@ -140,6 +142,8 @@ export abstract class ControlStatementNode extends StatementNode { }
export abstract class NonControlStatementNode extends StatementNode { }

export class VariableDefinitionNode extends NonControlStatementNode implements Named, Typed {
symbol?: Symbol;

constructor(
public type: Type,
public modifiers: Modifier[],
Expand Down
24 changes: 21 additions & 3 deletions packages/cashc/src/ast/SymbolTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,18 @@ import {
import { Modifier } from './Globals.js';
import { functionReturnType } from '../utils.js';

export enum ReferenceKind {
READ = 'read',
WRITE = 'write',
}

export interface Reference {
kind: ReferenceKind;
node: IdentifierNode;
}

export class Symbol {
references: IdentifierNode[] = [];
references: Reference[] = [];
inlinedFrame?: DebugFrame;

private constructor(
Expand All @@ -30,6 +40,14 @@ export class Symbol {
&& this.definition.modifiers.includes(modifier);
}

getReferences(kind: ReferenceKind): Reference[] {
return this.references.filter((reference) => reference.kind === kind);
}

isUnused(): boolean {
return this.getReferences(ReferenceKind.READ).length === 0;
}

static variable(node: VariableDefinitionNode | ParameterNode): Symbol {
return new Symbol(node.name, node.type, SymbolType.VARIABLE, node);
}
Expand Down Expand Up @@ -103,10 +121,10 @@ export class SymbolTable {
return `[${Array.from(this.symbols).map((e) => e[1])}]`;
}

unusedSymbols(): Symbol[] {
getUnmarkedUnusedSymbols(): Symbol[] {
return Array.from(this.symbols)
.map((e) => e[1])
.filter((s) => !s.hasModifier(Modifier.UNUSED))
.filter((s) => s.references.length === 0);
.filter((s) => s.isUnused());
}
}
10 changes: 9 additions & 1 deletion packages/cashc/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Ast } from './ast/AST.js';
import { checkVersionConstraints } from './ast/Pragma.js';
import { CashScriptErrorListener } from './ast/error-listeners.js';
import { MissingContractError } from './Errors.js';
import { CashScriptWarningListener, defaultWarningListener } from './Warnings.js';
import { parseCode } from './parser.js';
import {
createDiskResolver,
Expand All @@ -28,6 +29,7 @@ import {
import GenerateTargetTraversal from './generation/GenerateTargetTraversal.js';
import { FoldGlobalConstantsTraversal } from './semantic/FoldGlobalConstantsTraversal.js';
import SymbolTableTraversal from './semantic/SymbolTableTraversal.js';
import UnusedCodeWarningsTraversal from './semantic/UnusedCodeTraversal.js';
import TypeCheckTraversal from './semantic/TypeCheckTraversal.js';
import EnsureFinalRequireTraversal from './semantic/EnsureFinalRequireTraversal.js';
import EnsureFunctionsSafeTraversal from './semantic/EnsureFunctionsSafeTraversal.js';
Expand All @@ -42,6 +44,7 @@ export const DEFAULT_COMPILER_OPTIONS: CompilerOptions = {

export interface CompileOptions extends CompilerOptions {
errorListener?: CashScriptErrorListener;
warningListener?: CashScriptWarningListener;
}

export interface CompileStringOptions extends CompileOptions {
Expand All @@ -55,6 +58,8 @@ export interface CompileStringOptions extends CompileOptions {
* @param compilerOptions - Optional compiler options that override the defaults.
* @returns The compiled CashScript artifact, including ABI, bytecode and debug information.
* @throws If the source code contains a syntax, semantic, or type error, or an import cannot be resolved.
* @remarks Compilation warnings (e.g. unused variables) are passed to the `warningListener` compiler
* option, or printed with `console.warn` when no listener is provided.
*/
export const compileString: (code: string, compilerOptions?: CompileStringOptions) => Artifact =
compileStringInternal;
Expand Down Expand Up @@ -103,7 +108,7 @@ function compileCode(
resolver: ImportResolver,
compilerOptions: CompileOptions & InternalCompilerOptions,
): Artifact {
const { errorListener, disableInlining, ...artifactCompilerOptions } = compilerOptions;
const { errorListener, warningListener, disableInlining, ...artifactCompilerOptions } = compilerOptions;
const mergedCompilerOptions = { ...DEFAULT_COMPILER_OPTIONS, ...artifactCompilerOptions };

// Lexing + parsing
Expand All @@ -117,7 +122,10 @@ function compileCode(

// Semantic analysis
ast = ast.accept(new FoldGlobalConstantsTraversal()) as Ast;

ast = ast.accept(new SymbolTableTraversal()) as Ast;
ast = ast.accept(new UnusedCodeWarningsTraversal(warningListener ?? defaultWarningListener)) as Ast;

ast = ast.accept(new TypeCheckTraversal()) as Ast;
ast = ast.accept(new EnsureFunctionsSafeTraversal()) as Ast;
ast = ast.accept(new EnsureFinalRequireTraversal()) as Ast;
Expand Down
25 changes: 17 additions & 8 deletions packages/cashc/src/generation/GenerateTargetTraversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import {
ForNode,
} from '../ast/AST.js';
import AstTraversal from '../ast/AstTraversal.js';
import { GlobalFunction, Class, Modifier } from '../ast/Globals.js';
import { GlobalFunction, Class } from '../ast/Globals.js';
import { BinaryOperator } from '../ast/Operator.js';
import {
compileBinaryOp,
Expand Down Expand Up @@ -428,7 +428,7 @@ export default class GenerateTargetTraversal extends AstTraversal {

private dropUnusedParameters(parameters: ParameterNode[]): void {
parameters
.filter((parameter) => parameter.modifiers.includes(Modifier.UNUSED))
.filter((parameter) => parameter.symbol!.isUnused())
.sort((a, b) => this.getStackIndex(a.name) - this.getStackIndex(b.name))
.forEach((parameter) => {
const stackIndex = this.getStackIndex(parameter.name);
Expand Down Expand Up @@ -481,7 +481,7 @@ export default class GenerateTargetTraversal extends AstTraversal {
}

shouldEnforceFunctionParameterType(node: ParameterNode): boolean {
if (node.modifiers.includes(Modifier.UNUSED)) return false;
if (node.symbol!.isUnused()) return false;
if (node.type === PrimitiveType.BOOL) return true;
if (node.type instanceof BytesType && node.type.bound !== undefined) return true;
return false;
Expand All @@ -495,7 +495,7 @@ export default class GenerateTargetTraversal extends AstTraversal {
visitVariableDefinition(node: VariableDefinitionNode): Node {
node.expression = this.visit(node.expression);

if (node.modifiers.includes(Modifier.UNUSED)) {
if (node.symbol!.isUnused()) {
this.emit(Op.OP_DROP, { location: node.location, positionHint: PositionHint.END });
this.popFromStack();
return node;
Expand Down Expand Up @@ -523,10 +523,11 @@ export default class GenerateTargetTraversal extends AstTraversal {

const reversedTargets = [...node.targets].reverse();
reversedTargets.forEach((target) => {
if (target.isReassignment) {
this.emitReplace(this.getStackIndex(target.identifier.name), node);
} else if (target.modifiers.includes(Modifier.UNUSED)) {
// Unused variables are never added the stack, so their defined or re-assigned value is dropped
if (target.identifier.symbol!.isUnused()) {
this.emit(Op.OP_DROP, locationData);
} else if (target.isReassignment) {
this.emitReplace(this.getStackIndex(target.identifier.name), node);
} else {
this.emit(Op.OP_TOALTSTACK, locationData);
parkedDeclarations.push(target.identifier.name);
Expand All @@ -546,7 +547,7 @@ export default class GenerateTargetTraversal extends AstTraversal {
const locationData = { location: node.location, positionHint: PositionHint.END };

node.targets
.filter((target) => target.modifiers.includes(Modifier.UNUSED))
.filter((target) => target.identifier.symbol!.isUnused())
.sort((a, b) => this.getStackIndex(a.identifier.name) - this.getStackIndex(b.identifier.name))
.forEach((target) => {
const stackIndex = this.getStackIndex(target.identifier.name);
Expand All @@ -559,6 +560,14 @@ export default class GenerateTargetTraversal extends AstTraversal {

visitAssign(node: AssignNode): Node {
node.expression = this.visit(node.expression);

// An unused variable never gets added to the stack, so the assigned value is dropped as well
if (node.identifier.symbol!.isUnused()) {
this.emit(Op.OP_DROP, { location: node.location, positionHint: PositionHint.END });
this.popFromStack();
return node;
}

if (this.scopeDepth > 0) {
this.emitReplace(this.getStackIndex(node.identifier.name), node);
this.popFromStack();
Expand Down
1 change: 1 addition & 0 deletions packages/cashc/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './Errors.js';
export * from './Warnings.js';
export * as utils from '@cashscript/utils';
export {
compileFile, compileString, type CompileOptions, type CompileStringOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export class LowerGlobalConstantsTraversal extends AstTraversal {
identifier.location = node.location;
identifier.type = node.type;
identifier.symbol = symbol;
symbol.references.push(identifier);

const call = new FunctionCallNode(identifier, []);
call.location = node.location;
Expand Down
Loading
Loading