Detect unused variables

This commit is contained in:
nora 2024-05-13 20:02:08 +02:00
parent f164aad631
commit 9270f52e6b
33 changed files with 340 additions and 63 deletions

View file

@ -134,9 +134,8 @@ export type ItemKindFunction<P extends Phase> = {
};
export type FunctionArg<P extends Phase> = {
name: string;
ident: Ident;
type: Type<P>;
span: Span;
};
export type ItemKindType<P extends Phase> = {
@ -449,6 +448,8 @@ export type Resolution =
* ```
* When traversing resolutions, a stack of locals has to be kept.
* It's similar to a De Bruijn index.
*
* You generally want to index the stack as stack[stack.len - 1 - res.idx].
*/
index: number;
}
@ -577,10 +578,9 @@ export function superFoldItem<From extends Phase, To extends Phase>(
): Item<To> {
switch (item.kind) {
case "function": {
const args = item.params.map(({ name, type, span }) => ({
name,
const args = item.params.map(({ ident, type }) => ({
ident,
type: folder.type(type),
span,
}));
return {
@ -620,10 +620,9 @@ export function superFoldItem<From extends Phase, To extends Phase>(
};
}
case "import": {
const args = item.params.map(({ name, type, span }) => ({
name,
const args = item.params.map(({ ident, type }) => ({
ident,
type: folder.type(type),
span,
}));
return {
...item,

View file

@ -81,6 +81,7 @@ export type Options = {
packageName: string;
debug: Set<string>;
noOutput: boolean;
noStd: boolean;
};
export function parseArgs(hardcodedInput: string): Options {
@ -89,6 +90,7 @@ export function parseArgs(hardcodedInput: string): Options {
let packageName: string;
let debug = new Set<string>();
let noOutput = false;
let noStd = false;
if (process.argv.length > 2) {
filename = process.argv[2];
@ -117,6 +119,9 @@ export function parseArgs(hardcodedInput: string): Options {
if (process.argv.some((arg) => arg === "--no-output")) {
noOutput = true;
}
if (process.argv.some((arg) => arg === "--no-std")) {
noStd = true;
}
} else {
filename = "<hardcoded>";
input = hardcodedInput;
@ -136,5 +141,6 @@ export function parseArgs(hardcodedInput: string): Options {
packageName,
debug,
noOutput,
noStd,
};
}

View file

@ -44,12 +44,18 @@ export class ErrorHandler {
private emitter = (msg: string) => globalThis.console.error(msg),
) {}
public emit(err: CompilerError): ErrorEmitted {
renderError(this.emitter, err);
public emitError(err: CompilerError): ErrorEmitted {
renderDiagnostic(this.emitter, err, (msg) => chalk.red(`error: ${msg}`));
this.errors.push(err);
return ERROR_EMITTED;
}
public warn(err: CompilerError): void {
renderDiagnostic(this.emitter, err, (msg) =>
chalk.yellow(`warning: ${msg}`),
);
}
public hasErrors(): boolean {
return this.errors.length > 0;
}
@ -72,7 +78,11 @@ export class CompilerError {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const console = {};
function renderError(emitter: Emitter, e: CompilerError) {
function renderDiagnostic(
emitter: Emitter,
e: CompilerError,
render_msg: (msg: string) => string,
) {
const { span } = e;
const { content } = span.file;
@ -88,7 +98,7 @@ function renderError(emitter: Emitter, e: CompilerError) {
}
const lineIdx = lineSpans.indexOf(line);
const lineNo = lineIdx + 1;
emitter(chalk.red(`error: ${e.msg}`));
emitter(render_msg(e.msg));
emitter(` --> ${span.file.path ?? "<unknown>"}:${lineNo}`);
emitter(`${lineNo} | ${spanToSnippet(content, line)}`);

View file

@ -44,7 +44,7 @@ function main() {
const start = Date.now();
if (packageName !== "std") {
if (packageName !== "std" && !opts.noStd) {
gcx.pkgLoader(gcx, "std", Span.startOfFile(file));
}

View file

@ -114,7 +114,7 @@ export function tokenize(handler: ErrorHandler, file: LoadedFile): LexerResult {
return { ok: true, tokens: tokenizeInner(file) };
} catch (e) {
if (e instanceof LexerError) {
const err: ErrorEmitted = handler.emit(e.inner);
const err: ErrorEmitted = handler.emitError(e.inner);
return { ok: false, err };
} else {
throw e;

View file

@ -104,7 +104,7 @@ export const loadPkg: PkgLoader = (
return dummyErrorPkg(
pkgId,
name,
gcx.error.emit(
gcx.error.emitError(
new CompilerError(`cycle detected loading extern module ${name}`, span),
),
);
@ -118,7 +118,7 @@ export const loadPkg: PkgLoader = (
const file = loadModuleFile(".", name, span);
if (!file.ok) {
return dummyErrorPkg(pkgId, name, gcx.error.emit(file.err));
return dummyErrorPkg(pkgId, name, gcx.error.emitError(file.err));
}
const tokens = tokenize(gcx.error, file.value);

View file

@ -233,7 +233,7 @@ function parseItem(t: State): [State, Item<Parsed>] {
[t] = expectNext(t, ")");
} else {
if (name.span.file.path === undefined) {
t.gcx.error.emit(
t.gcx.error.emitError(
new CompilerError(
`no known source file for statement, cannot load file relative to it`,
name.span,
@ -245,7 +245,7 @@ function parseItem(t: State): [State, Item<Parsed>] {
const file = loadModuleFile(name.span.file.path, name.ident, name.span);
if (!file.ok) {
t.gcx.error.emit(file.err);
t.gcx.error.emitError(file.err);
contents = [];
} else {
const tokens = tokenize(t.gcx.error, file.value);
@ -311,15 +311,19 @@ function parseFunctionSig(t: State): [State, FunctionSig] {
[t] = expectNext(t, "(");
let params: FunctionArg<Parsed>[];
[t, params] = parseCommaSeparatedList(t, ")", (t) => {
let name;
[t, name] = expectNext<TokenIdent>(t, "identifier");
[t] = expectNext(t, ":");
let type;
[t, type] = parseType(t);
[t, params] = parseCommaSeparatedList(
t,
")",
(t): [State, FunctionArg<Parsed>] => {
let name;
[t, name] = expectNext<TokenIdent>(t, "identifier");
[t] = expectNext(t, ":");
let type;
[t, type] = parseType(t);
return [t, { name: name.ident, type, span: name.span }];
});
return [t, { ident: { name: name.ident, span: name.span }, type }];
},
);
let colon;
let returnType = undefined;
@ -732,7 +736,7 @@ function parseType(t: State): [State, Type<Parsed>] {
}
default: {
throw new FatalParseError(
t.gcx.error.emit(
t.gcx.error.emitError(
new CompilerError(
`unexpected token: \`${tok.kind}\`, expected type`,
span,
@ -799,7 +803,7 @@ function expectNext<T extends BaseToken>(
[t, tok] = maybeNextT(t);
if (!tok) {
throw new FatalParseError(
t.gcx.error.emit(
t.gcx.error.emitError(
new CompilerError(
`expected \`${kind}\`, found end of file`,
Span.eof(t.file),
@ -809,7 +813,7 @@ function expectNext<T extends BaseToken>(
}
if (tok.kind !== kind) {
throw new FatalParseError(
t.gcx.error.emit(
t.gcx.error.emitError(
new CompilerError(
`expected \`${kind}\`, found \`${tok.kind}\``,
tok.span,
@ -824,7 +828,7 @@ function next(t: State): [State, Token] {
const [rest, next] = maybeNextT(t);
if (!next) {
throw new FatalParseError(
t.gcx.error.emit(
t.gcx.error.emitError(
new CompilerError("unexpected end of file", Span.eof(t.file)),
),
);
@ -841,7 +845,7 @@ function maybeNextT(t: State): [State, Token | undefined] {
function unexpectedToken(t: ParseState, token: Token, expected: string): never {
throw new FatalParseError(
t.gcx.error.emit(
t.gcx.error.emitError(
new CompilerError(`unexpected token, expected ${expected}`, token.span),
),
);
@ -875,7 +879,7 @@ function validateAst(ast: Pkg<Built>, gcx: GlobalContext) {
});
return expr;
} else if (expr.kind === "let") {
gcx.error.emit(
gcx.error.emitError(
new CompilerError("let is only allowed in blocks", expr.span),
);
return superFoldExpr(expr, this);
@ -886,7 +890,7 @@ function validateAst(ast: Pkg<Built>, gcx: GlobalContext) {
const innerClass = binaryExprPrecedenceClass(inner.binaryKind);
if (ourClass !== innerClass) {
gcx.error.emit(
gcx.error.emitError(
new CompilerError(
`mixing operators without parentheses is not allowed. ${side} is ${inner.binaryKind}, which is different from ${expr.binaryKind}`,
expr.span,

View file

@ -57,7 +57,7 @@ function printItem(item: Item<AnyPhase>): string {
function printFunction(func: ItemFunction<AnyPhase>): string {
const args = func.params
.map(({ name, type }) => `${name}: ${printType(type)}`)
.map(({ ident: name, type }) => `${name}: ${printType(type)}`)
.join(", ");
const ret = func.returnType ? `: ${printType(func.returnType)}` : "";
return `function ${func.name}(${args})${ret} = ${printExpr(func.body, 0)};`;
@ -89,7 +89,7 @@ function printTypeDef(type: ItemType<AnyPhase>): string {
function printImportDef(def: ItemImport<AnyPhase>): string {
const args = def.params
.map(({ name, type }) => `${name}: ${printType(type)}`)
.map(({ ident: name, type }) => `${name}: ${printType(type)}`)
.join(", ");
const ret = def.returnType ? `: ${printType(def.returnType)}` : "";

View file

@ -92,7 +92,7 @@ function resolveModule(
contents.forEach((item) => {
const existing = items.get(item.name);
if (existing !== undefined) {
cx.gcx.error.emit(
cx.gcx.error.emitError(
new CompilerError(
`item \`${item.name}\` has already been declared`,
item.span,
@ -164,7 +164,7 @@ function resolveModule(
return {
kind: "error",
err: cx.gcx.error.emit(
err: cx.gcx.error.emitError(
new CompilerError(`cannot find ${ident.name}`, ident.span),
),
};
@ -179,18 +179,17 @@ function resolveModule(
switch (item.kind) {
case "function": {
const params = item.params.map(({ name, span, type }) => ({
name,
span,
const params = item.params.map(({ ident, type }) => ({
ident,
type: this.type(type),
}));
const returnType = item.returnType && this.type(item.returnType);
item.params.forEach(({ name }) => scopes.push(name));
item.params.forEach(({ ident: name }) => scopes.push(name.name));
const body = this.expr(item.body);
const revParams = item.params.slice();
revParams.reverse();
revParams.forEach(({ name }) => popScope(name));
revParams.forEach(({ ident: name }) => popScope(name.name));
return {
kind: "function",
@ -289,7 +288,7 @@ function resolveModule(
let pathRes: Resolution;
if (typeof expr.field.value === "number") {
const err: ErrorEmitted = cx.gcx.error.emit(
const err: ErrorEmitted = cx.gcx.error.emitError(
new CompilerError(
"module contents cannot be indexed with a number",
expr.field.span,
@ -304,7 +303,7 @@ function resolveModule(
);
if (pathResItem === undefined) {
const err: ErrorEmitted = cx.gcx.error.emit(
const err: ErrorEmitted = cx.gcx.error.emitError(
new CompilerError(
`module ${module.name} has no item ${expr.field.value}`,
expr.field.span,

View file

@ -32,5 +32,5 @@ export function tyErrorFrom(prev: { err: ErrorEmitted }): Ty {
}
export function emitError(cx: TypeckCtx, err: CompilerError): ErrorEmitted {
return cx.gcx.error.emit(err);
return cx.gcx.error.emitError(err);
}

View file

@ -17,6 +17,7 @@ import { emitError } from "./base";
import { checkBody, exprError } from "./expr";
import { InferContext } from "./infer";
import { typeOfItem } from "./item";
import { lintProgram } from "./lint";
export function typeck(gcx: GlobalContext, ast: Pkg<Resolved>): Pkg<Typecked> {
const cx = {
@ -55,7 +56,7 @@ export function typeck(gcx: GlobalContext, ast: Pkg<Resolved>): Pkg<Typecked> {
cx,
new CompilerError(
`import parameters must be I32 or Int`,
item.params[i].span,
item.params[i].ident.span,
),
);
}
@ -214,5 +215,7 @@ export function typeck(gcx: GlobalContext, ast: Pkg<Resolved>): Pkg<Typecked> {
}
}
lintProgram(gcx, typecked);
return typecked;
}

View file

@ -162,7 +162,7 @@ export class InferContext {
}
}
this.error.emit(
this.error.emitError(
new CompilerError(
`cannot assign ${printTy(rhs)} to ${printTy(lhs)}`,
span,

108
src/typeck/lint.ts Normal file
View file

@ -0,0 +1,108 @@
import {
Folder,
Ident,
Item,
Pkg,
Typecked,
foldAst,
mkDefaultFolder,
superFoldExpr,
superFoldItem,
} from "../ast";
import { GlobalContext } from "../context";
import { CompilerError } from "../error";
type LintContext = {
locals: LocalUsed[];
};
type LocalUsed = {
ident: Ident;
used: boolean;
isParam: boolean;
};
export function lintProgram(gcx: GlobalContext, ast: Pkg<Typecked>): void {
const cx: LintContext = {
locals: [],
};
const checker: Folder<Typecked, Typecked> = {
...mkDefaultFolder(),
itemInner(item: Item<Typecked>): Item<Typecked> {
switch (item.kind) {
case "function": {
const prev = cx.locals;
cx.locals = [
...item.params.map((param) => ({
ident: param.ident,
used: false,
isParam: true,
})),
];
superFoldItem(item, this);
checkLocalsUsed(gcx, 0, cx);
cx.locals = prev;
return item;
}
}
return superFoldItem(item, this);
},
expr(expr) {
switch (expr.kind) {
case "let": {
superFoldExpr(expr, this);
cx.locals.push({ ident: expr.name, used: false, isParam: false });
return expr;
}
case "block": {
const prevLocalsLen = cx.locals.length;
superFoldExpr(expr, this);
checkLocalsUsed(gcx, prevLocalsLen, cx);
cx.locals.length = prevLocalsLen;
return expr;
}
case "ident": {
const { res } = expr.value;
switch (res.kind) {
case "local": {
cx.locals[cx.locals.length - 1 - res.index].used = true;
break;
}
}
return superFoldExpr(expr, this);
}
}
return superFoldExpr(expr, this);
},
ident(ident) {
return ident;
},
type(type) {
return type;
},
};
foldAst(ast, checker);
}
function checkLocalsUsed(
gcx: GlobalContext,
prevLength: number,
cx: LintContext,
) {
for (let i = prevLength; i < cx.locals.length; i++) {
const local = cx.locals[i];
if (!local.used && !local.ident.name.startsWith("_")) {
const kind = local.isParam ? "function parameter" : "variable";
gcx.error.warn(
new CompilerError(
`unused ${kind}: \`${local.ident.name}\``,
local.ident.span,
),
);
}
}
}

View file

@ -2,5 +2,6 @@ import ("wasi_snapshot_preview1" "fd_write")
fd_write(fd: I32, ciovec_ptr: I32, ciovec_len: I32, out_ptr: I32): I32;
function print(s: String) = (
let s: (I32, I32) = ___transmute(s);
// TODO: do it
let _s: (I32, I32) = ___transmute(s);
);

View file

@ -29,7 +29,7 @@ function allocate(size: I32, align: I32): I32 = (
alignedPtr
);
function deallocate(ptr: I32, size: I32) = (
function deallocate(_ptr: I32, _size: I32) = (
std.println("uwu deawwocate :3");
);

View file

@ -1,6 +1,7 @@
mod alloc;
function memcpy(dst: I32, src: I32, n: I32) =
// The function parameters are not actually unused.
function memcpy(_dst: I32, _src: I32, _n: I32) =
___asm(
__locals(),
"local.get 2",

View file

@ -1,6 +1,6 @@
//@check-pass
function dropping(a: I32) =
function dropping(_a: I32) =
___asm(
__locals(),
"local.get 0",

View file

@ -1,4 +1,4 @@
function a(a: I32) =
function a(_a: I32) =
___asm(
__locals(),
0,

View file

@ -1,4 +1,4 @@
function dropping(a: I32) =
function dropping(_a: I32) =
___asm(
__locals(),
"meow meow",

View file

@ -1,4 +1,4 @@
function dropping(a: I32) =
function dropping(_a: I32) =
___asm(
"local.get 0",
"drop",

View file

@ -1,4 +1,4 @@
function dropping(a: I32) = (
function dropping(_a: I32) = (
1;
___asm(__locals(), "drop");
);

View file

@ -1,11 +1,11 @@
function a(a: I32) =
function a(_a: I32) =
___asm(
__locals(),
"local.get 0 0",
"drop",
);
function b(a: I32) =
function b(_a: I32) =
___asm(
__locals(),
"local.get",

View file

@ -10,3 +10,11 @@ error: cannot assign String to Int
--> $DIR/basic_recovery.nil:3
3 | let b: Int = "";
^
warning: unused variable: `a`
--> $DIR/basic_recovery.nil:2
2 | let a: Int = "";
^
warning: unused variable: `b`
--> $DIR/basic_recovery.nil:3
3 | let b: Int = "";
^

View file

@ -5,14 +5,14 @@ function main() = (
singleArg("hi!");
manyArgs(1,2,3,4,5,6);
let a: () = returnNothing();
let b: () = returnExplicitUnit();
let c: String = returnString();
let _a: () = returnNothing();
let _b: () = returnExplicitUnit();
let _c: String = returnString();
);
function noArgs() =;
function singleArg(a: String) =;
function manyArgs(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) =;
function singleArg(_a: String) =;
function manyArgs(_a: Int, _b: Int, _c: Int, _d: Int, _e: Int, _f: Int) =;
function returnNothing() =;
function returnExplicitUnit(): () =;

View file

@ -2,4 +2,4 @@ function main() = (
x();
);
function x(a: Int) = ;
function x(_a: Int) = ;

View file

@ -0,0 +1,22 @@
//@check-pass
function main() = (
let x = 0;
let _ok = 0;
let used = 0;
used;
);
function x() = (
let x = 0;
(
let x = 0;
call(x);
);
let y = x;
);
function call(_a: Int) = ;
function param(p: Int) = ;

View file

@ -0,0 +1,12 @@
warning: unused variable: `x`
--> $DIR/unused_vars.nil:4
4 | let x = 0;
^
warning: unused variable: `y`
--> $DIR/unused_vars.nil:17
17 | let y = x;
^
warning: unused function parameter: `p`
--> $DIR/unused_vars.nil:22
22 | function param(p: Int) = ;
^

View file

@ -2,3 +2,7 @@ error: type I32 does not take any generic arguments but 1 were passed
--> $DIR/generics_on_primitive.nil:2
2 | let a: I32[I32] = 0;
^^^
warning: unused variable: `a`
--> $DIR/generics_on_primitive.nil:2
2 | let a: I32[I32] = 0;
^

View file

@ -0,0 +1,12 @@
warning: unused function parameter: `a`
--> $DIR/generics_structs_in_args.nil:11
11 | function test(a: A[I32], b: B[I32, Int, I32], c: C) = ;
^
warning: unused function parameter: `b`
--> $DIR/generics_structs_in_args.nil:11
11 | function test(a: A[I32], b: B[I32, Int, I32], c: C) = ;
^
warning: unused function parameter: `c`
--> $DIR/generics_structs_in_args.nil:11
11 | function test(a: A[I32], b: B[I32, Int, I32], c: C) = ;
^

View file

@ -0,0 +1,12 @@
warning: unused function parameter: `a`
--> $DIR/structs.nil:9
9 | function test(a: A[I32], b: B[I32, Int, I32], c: C) = ;
^
warning: unused function parameter: `b`
--> $DIR/structs.nil:9
9 | function test(a: A[I32], b: B[I32, Int, I32], c: C) = ;
^
warning: unused function parameter: `c`
--> $DIR/structs.nil:9
9 | function test(a: A[I32], b: B[I32, Int, I32], c: C) = ;
^

View file

@ -22,3 +22,51 @@ error: type () does not take any generic arguments but 1 were passed
--> $DIR/wrong_amount.nil:22
22 | c3: C[I32],
^
warning: unused function parameter: `a1`
--> $DIR/wrong_amount.nil:9
9 | a1: A,
^^
warning: unused function parameter: `a2`
--> $DIR/wrong_amount.nil:10
10 | a2: A[],
^^
warning: unused function parameter: `a3`
--> $DIR/wrong_amount.nil:11
11 | a3: A[I32],
^^
warning: unused function parameter: `a4`
--> $DIR/wrong_amount.nil:12
12 | a4: A[I32, I32],
^^
warning: unused function parameter: `b1`
--> $DIR/wrong_amount.nil:14
14 | b1: B,
^^
warning: unused function parameter: `b2`
--> $DIR/wrong_amount.nil:15
15 | b2: B[],
^^
warning: unused function parameter: `b3`
--> $DIR/wrong_amount.nil:16
16 | b3: B[Int, Int],
^^
warning: unused function parameter: `b4`
--> $DIR/wrong_amount.nil:17
17 | b4: B[Int, I32, Int],
^^
warning: unused function parameter: `b5`
--> $DIR/wrong_amount.nil:18
18 | b5: B[Int, Int, Int, Int],
^^
warning: unused function parameter: `c1`
--> $DIR/wrong_amount.nil:20
20 | c1: C,
^^
warning: unused function parameter: `c2`
--> $DIR/wrong_amount.nil:21
21 | c2: C[],
^^
warning: unused function parameter: `c3`
--> $DIR/wrong_amount.nil:22
22 | c3: C[I32],
^^

View file

@ -0,0 +1,4 @@
warning: unused variable: `a`
--> $DIR/type_alias.nil:6
6 | let a: A = (0, 0);
^

View file

@ -0,0 +1,24 @@
warning: unused variable: `a1`
--> $DIR/type_assignments.nil:15
15 | let a1: Int = a;
^^
warning: unused variable: `b1`
--> $DIR/type_assignments.nil:16
16 | let b1: I32 = b;
^^
warning: unused variable: `c1`
--> $DIR/type_assignments.nil:17
17 | let c1: String = c;
^^
warning: unused variable: `d1`
--> $DIR/type_assignments.nil:18
18 | let d1: Bool = d;
^^
warning: unused variable: `e1`
--> $DIR/type_assignments.nil:19
19 | let e1: CustomType = e;
^^
warning: unused variable: `f1`
--> $DIR/type_assignments.nil:20
20 | let f1: (Int, I32) = f;
^^