From 9270f52e6bac1a8521b78521063383dddd0ef5c3 Mon Sep 17 00:00:00 2001
From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com>
Date: Mon, 13 May 2024 20:02:08 +0200
Subject: [PATCH] Detect unused variables
---
src/ast.ts | 15 ++-
src/context.ts | 6 +
src/error.ts | 18 ++-
src/index.ts | 2 +-
src/lexer.ts | 2 +-
src/loader.ts | 4 +-
src/parser.ts | 38 +++---
src/printer.ts | 4 +-
src/resolve.ts | 17 ++-
src/typeck/base.ts | 2 +-
src/typeck/index.ts | 5 +-
src/typeck/infer.ts | 2 +-
src/typeck/lint.ts | 108 ++++++++++++++++++
std/io.nil | 3 +-
std/rt/alloc.nil | 2 +-
std/rt/rt.mod.nil | 3 +-
ui-tests/asm/drop.nil | 2 +-
ui-tests/asm/instr_not_string.nil | 2 +-
ui-tests/asm/invalid_instr.nil | 2 +-
ui-tests/asm/missing_locals.nil | 2 +-
ui-tests/asm/not_toplevel.nil | 2 +-
ui-tests/asm/wrong_imm.nil | 4 +-
ui-tests/basic_recovery.stderr | 8 ++
ui-tests/functions/function_calls_ok.nil | 10 +-
ui-tests/functions/missing_args.nil | 2 +-
ui-tests/lint/unused_vars.nil | 22 ++++
ui-tests/lint/unused_vars.stderr | 12 ++
.../generics/generics_on_primitive.stderr | 4 +
.../generics/generics_structs_in_args.stderr | 12 ++
ui-tests/type/generics/structs.stderr | 12 ++
ui-tests/type/generics/wrong_amount.stderr | 48 ++++++++
ui-tests/type/type_alias.stderr | 4 +
ui-tests/type/type_assignments.stderr | 24 ++++
33 files changed, 340 insertions(+), 63 deletions(-)
create mode 100644 src/typeck/lint.ts
create mode 100644 ui-tests/lint/unused_vars.nil
create mode 100644 ui-tests/lint/unused_vars.stderr
create mode 100644 ui-tests/type/generics/generics_structs_in_args.stderr
create mode 100644 ui-tests/type/generics/structs.stderr
create mode 100644 ui-tests/type/type_alias.stderr
create mode 100644 ui-tests/type/type_assignments.stderr
diff --git a/src/ast.ts b/src/ast.ts
index 2ba7f51..eb7777f 100644
--- a/src/ast.ts
+++ b/src/ast.ts
@@ -134,9 +134,8 @@ export type ItemKindFunction
= {
};
export type FunctionArg
= {
- name: string;
+ ident: Ident;
type: Type
;
- span: Span;
};
export type ItemKindType
= {
@@ -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(
): Item {
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(
};
}
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,
diff --git a/src/context.ts b/src/context.ts
index aa61f46..d16cf3f 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -81,6 +81,7 @@ export type Options = {
packageName: string;
debug: Set;
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();
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 = "";
input = hardcodedInput;
@@ -136,5 +141,6 @@ export function parseArgs(hardcodedInput: string): Options {
packageName,
debug,
noOutput,
+ noStd,
};
}
diff --git a/src/error.ts b/src/error.ts
index 35fee9b..7da909e 100644
--- a/src/error.ts
+++ b/src/error.ts
@@ -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 ?? ""}:${lineNo}`);
emitter(`${lineNo} | ${spanToSnippet(content, line)}`);
diff --git a/src/index.ts b/src/index.ts
index f962bfa..1d96ed2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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));
}
diff --git a/src/lexer.ts b/src/lexer.ts
index 276f98c..537fa0e 100644
--- a/src/lexer.ts
+++ b/src/lexer.ts
@@ -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;
diff --git a/src/loader.ts b/src/loader.ts
index eae8f88..2f3fafa 100644
--- a/src/loader.ts
+++ b/src/loader.ts
@@ -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);
diff --git a/src/parser.ts b/src/parser.ts
index 6786c04..c3ac6cb 100644
--- a/src/parser.ts
+++ b/src/parser.ts
@@ -233,7 +233,7 @@ function parseItem(t: State): [State, Item] {
[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] {
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[];
- [t, params] = parseCommaSeparatedList(t, ")", (t) => {
- let name;
- [t, name] = expectNext(t, "identifier");
- [t] = expectNext(t, ":");
- let type;
- [t, type] = parseType(t);
+ [t, params] = parseCommaSeparatedList(
+ t,
+ ")",
+ (t): [State, FunctionArg] => {
+ let name;
+ [t, name] = expectNext(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] {
}
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, 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(
}
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, 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, 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,
diff --git a/src/printer.ts b/src/printer.ts
index 977d578..6e980ad 100644
--- a/src/printer.ts
+++ b/src/printer.ts
@@ -57,7 +57,7 @@ function printItem(item: Item): string {
function printFunction(func: ItemFunction): 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): string {
function printImportDef(def: ItemImport): 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)}` : "";
diff --git a/src/resolve.ts b/src/resolve.ts
index 2cedad3..9d41d03 100644
--- a/src/resolve.ts
+++ b/src/resolve.ts
@@ -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,
diff --git a/src/typeck/base.ts b/src/typeck/base.ts
index 83d0a62..0eaed1a 100644
--- a/src/typeck/base.ts
+++ b/src/typeck/base.ts
@@ -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);
}
diff --git a/src/typeck/index.ts b/src/typeck/index.ts
index b247b18..d7ea1a6 100644
--- a/src/typeck/index.ts
+++ b/src/typeck/index.ts
@@ -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): Pkg {
const cx = {
@@ -55,7 +56,7 @@ export function typeck(gcx: GlobalContext, ast: Pkg): Pkg {
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): Pkg {
}
}
+ lintProgram(gcx, typecked);
+
return typecked;
}
diff --git a/src/typeck/infer.ts b/src/typeck/infer.ts
index 5a4fa8e..7cdafe8 100644
--- a/src/typeck/infer.ts
+++ b/src/typeck/infer.ts
@@ -162,7 +162,7 @@ export class InferContext {
}
}
- this.error.emit(
+ this.error.emitError(
new CompilerError(
`cannot assign ${printTy(rhs)} to ${printTy(lhs)}`,
span,
diff --git a/src/typeck/lint.ts b/src/typeck/lint.ts
new file mode 100644
index 0000000..857149b
--- /dev/null
+++ b/src/typeck/lint.ts
@@ -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): void {
+ const cx: LintContext = {
+ locals: [],
+ };
+
+ const checker: Folder = {
+ ...mkDefaultFolder(),
+ itemInner(item: Item): Item {
+ 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,
+ ),
+ );
+ }
+ }
+}
diff --git a/std/io.nil b/std/io.nil
index cb48293..d8213e2 100644
--- a/std/io.nil
+++ b/std/io.nil
@@ -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);
);
diff --git a/std/rt/alloc.nil b/std/rt/alloc.nil
index d6f2709..b742475 100644
--- a/std/rt/alloc.nil
+++ b/std/rt/alloc.nil
@@ -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");
);
diff --git a/std/rt/rt.mod.nil b/std/rt/rt.mod.nil
index 28cd3bc..318b4cf 100644
--- a/std/rt/rt.mod.nil
+++ b/std/rt/rt.mod.nil
@@ -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",
diff --git a/ui-tests/asm/drop.nil b/ui-tests/asm/drop.nil
index 6025e29..94047ec 100644
--- a/ui-tests/asm/drop.nil
+++ b/ui-tests/asm/drop.nil
@@ -1,6 +1,6 @@
//@check-pass
-function dropping(a: I32) =
+function dropping(_a: I32) =
___asm(
__locals(),
"local.get 0",
diff --git a/ui-tests/asm/instr_not_string.nil b/ui-tests/asm/instr_not_string.nil
index 2ac1287..0018d3d 100644
--- a/ui-tests/asm/instr_not_string.nil
+++ b/ui-tests/asm/instr_not_string.nil
@@ -1,4 +1,4 @@
-function a(a: I32) =
+function a(_a: I32) =
___asm(
__locals(),
0,
diff --git a/ui-tests/asm/invalid_instr.nil b/ui-tests/asm/invalid_instr.nil
index 58a9a50..c383579 100644
--- a/ui-tests/asm/invalid_instr.nil
+++ b/ui-tests/asm/invalid_instr.nil
@@ -1,4 +1,4 @@
-function dropping(a: I32) =
+function dropping(_a: I32) =
___asm(
__locals(),
"meow meow",
diff --git a/ui-tests/asm/missing_locals.nil b/ui-tests/asm/missing_locals.nil
index bd7d66e..95a0937 100644
--- a/ui-tests/asm/missing_locals.nil
+++ b/ui-tests/asm/missing_locals.nil
@@ -1,4 +1,4 @@
-function dropping(a: I32) =
+function dropping(_a: I32) =
___asm(
"local.get 0",
"drop",
diff --git a/ui-tests/asm/not_toplevel.nil b/ui-tests/asm/not_toplevel.nil
index 438304e..ddff7e1 100644
--- a/ui-tests/asm/not_toplevel.nil
+++ b/ui-tests/asm/not_toplevel.nil
@@ -1,4 +1,4 @@
-function dropping(a: I32) = (
+function dropping(_a: I32) = (
1;
___asm(__locals(), "drop");
);
diff --git a/ui-tests/asm/wrong_imm.nil b/ui-tests/asm/wrong_imm.nil
index 3329781..1ed6d5a 100644
--- a/ui-tests/asm/wrong_imm.nil
+++ b/ui-tests/asm/wrong_imm.nil
@@ -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",
diff --git a/ui-tests/basic_recovery.stderr b/ui-tests/basic_recovery.stderr
index e933ad1..aa671f2 100644
--- a/ui-tests/basic_recovery.stderr
+++ b/ui-tests/basic_recovery.stderr
@@ -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 = "";
+ ^
diff --git a/ui-tests/functions/function_calls_ok.nil b/ui-tests/functions/function_calls_ok.nil
index 0deb8cf..11b59ce 100644
--- a/ui-tests/functions/function_calls_ok.nil
+++ b/ui-tests/functions/function_calls_ok.nil
@@ -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(): () =;
diff --git a/ui-tests/functions/missing_args.nil b/ui-tests/functions/missing_args.nil
index d247851..857fa31 100644
--- a/ui-tests/functions/missing_args.nil
+++ b/ui-tests/functions/missing_args.nil
@@ -2,4 +2,4 @@ function main() = (
x();
);
-function x(a: Int) = ;
\ No newline at end of file
+function x(_a: Int) = ;
\ No newline at end of file
diff --git a/ui-tests/lint/unused_vars.nil b/ui-tests/lint/unused_vars.nil
new file mode 100644
index 0000000..356b3f5
--- /dev/null
+++ b/ui-tests/lint/unused_vars.nil
@@ -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) = ;
diff --git a/ui-tests/lint/unused_vars.stderr b/ui-tests/lint/unused_vars.stderr
new file mode 100644
index 0000000..ad493fa
--- /dev/null
+++ b/ui-tests/lint/unused_vars.stderr
@@ -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) = ;
+ ^
diff --git a/ui-tests/type/generics/generics_on_primitive.stderr b/ui-tests/type/generics/generics_on_primitive.stderr
index d4019c2..887b9c2 100644
--- a/ui-tests/type/generics/generics_on_primitive.stderr
+++ b/ui-tests/type/generics/generics_on_primitive.stderr
@@ -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;
+ ^
diff --git a/ui-tests/type/generics/generics_structs_in_args.stderr b/ui-tests/type/generics/generics_structs_in_args.stderr
new file mode 100644
index 0000000..d70b550
--- /dev/null
+++ b/ui-tests/type/generics/generics_structs_in_args.stderr
@@ -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) = ;
+ ^
diff --git a/ui-tests/type/generics/structs.stderr b/ui-tests/type/generics/structs.stderr
new file mode 100644
index 0000000..3597880
--- /dev/null
+++ b/ui-tests/type/generics/structs.stderr
@@ -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) = ;
+ ^
diff --git a/ui-tests/type/generics/wrong_amount.stderr b/ui-tests/type/generics/wrong_amount.stderr
index 4728cd1..03675ee 100644
--- a/ui-tests/type/generics/wrong_amount.stderr
+++ b/ui-tests/type/generics/wrong_amount.stderr
@@ -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],
+ ^^
diff --git a/ui-tests/type/type_alias.stderr b/ui-tests/type/type_alias.stderr
new file mode 100644
index 0000000..46551de
--- /dev/null
+++ b/ui-tests/type/type_alias.stderr
@@ -0,0 +1,4 @@
+warning: unused variable: `a`
+ --> $DIR/type_alias.nil:6
+6 | let a: A = (0, 0);
+ ^
diff --git a/ui-tests/type/type_assignments.stderr b/ui-tests/type/type_assignments.stderr
new file mode 100644
index 0000000..60ecfea
--- /dev/null
+++ b/ui-tests/type/type_assignments.stderr
@@ -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;
+ ^^