Feature: Variable usage checker (#32)

* Dependency

- Added `varRefCounts` which maps a given `Variable` to its reference count. This includes the declaration thereof.
- Added `touch(Variable variable)` which Increments the given variable's reference count

* Dependency

- When handling a standalone assignment to a variable increment it's reference count
- This was updated in the handling of `VariableAssignmentStdAlone`

* Dependency

- Make variable declaration count as the first reference

* Dependency

- VAriables that appear in expressions must increase the ref count

* Dependency

- Added `getUnusedVariables()` which returns all variables which were declared but not used

* TypeChecker

- Moved the reference counting mechanism into the `TypeChecker` to avoid problem of seperate `DNodeGenerator` instances having their own seperate counts
- We now check the reference count values after function definition processing so as to get a proper count

Dependency

- Use the reference counting from `TypeChecker`
- Removed reference counting code

Test cases

- Added a positive case in the form of `unused_vars.t`
- Added a negative case in the form of `unused_vars_none.t`

* Commands

- Added commands to `TypeCheckerBase!()` mixin template
- Added `TypeCheckerInit(Compiler compiler)`
- Use the template in both `compile` command and the`typecheck` command

* TypeChecker

- We now respect the `typecheck:warnUnusedVars` option

* TypeChecker

- Added `doPostChecks()`
- Moved the reference counting code to `doPostChecks()`

* Configuration

- `defaultConfig()` now adds an entry for `typecheck:warnUnusedVars` set to `true`

* TypeChecker

- Use `getName()` on the `Variable` when printing out the names of unused variables

* Compiler

- Made `gibFileData(string sourceFile)` public
- Added `getTypeChecker()` to the `Compiler` class

* TypeChecker (unittests)

- Added a positive test case unittest for the unused variables detection mechanism

* TypeChecker (unittests)

- Check the variable is equal to the one we are looking for (`j`)

* TypeChecker (unittests)

- Added a negative test case for the unused variables detection mechanism

* Pipelines

- Added tests for `unused_vars.t` and `unused_vars_none.t`

* TypeChecker

- Fixed message which prints out when an unused variable is detected

* Merge branch 'vardec_varass_dependency' into feature/unused_vars_detection
This commit is contained in:
Tristan B. Velloza Kildaire 2024-04-01 21:55:56 +02:00 committed by GitHub
parent 7a2ea96140
commit 800f06da5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 197 additions and 4 deletions

View File

@ -382,6 +382,11 @@ jobs:
else
exit 1
fi
- name: Unused variables check (positive case)
run: ./tlang typecheck source/tlang/testing/unused_vars.t --unusedVars true
- name: Unused variables check (negative case)
run: ./tlang typecheck source/tlang/testing/unused_vars_none.t --unusedVars true
emit:
needs: [build, unittests]

View File

@ -116,7 +116,15 @@ mixin template EmitBase()
*/
mixin template TypeCheckerBase()
{
@ArgNamed("unusedVars|uvars", "Warn about any unused variables")
@(ArgConfig.optional)
bool warnUnusedVariables = true;
void TypeCheckerInit(Compiler compiler)
{
// Set whether to warn about unused variables
compiler.getConfig().addConfig(ConfigEntry("typecheck:warnUnusedVars", warnUnusedVariables));
}
}
/**
@ -127,7 +135,7 @@ struct compileCommand
{
mixin BaseCommand!();
mixin TypeCheckerBase!();
mixin EmitBase!();
@ -154,6 +162,9 @@ struct compileCommand
/* Setup general configuration parameters */
BaseCommandInit(compiler);
/* Setup all type checker related parameters */
TypeCheckerInit(compiler);
/* Perform tokenization */
compiler.doLex();
writeln("=== Tokens ===\n");
@ -295,7 +306,7 @@ struct parseCommand
struct typecheckCommand
{
mixin BaseCommand!();
mixin TypeCheckerBase!();
void onExecute()
{
@ -317,6 +328,9 @@ struct typecheckCommand
/* Setup general configuration parameters */
BaseCommandInit(compiler);
/* Setup all type checker related parameters */
TypeCheckerInit(compiler);
compiler.doLex();
writeln("=== Tokens ===\n");
writeln(compiler.getTokens());

View File

@ -229,6 +229,9 @@ public final class CompilerConfiguration
config.addConfig(ConfigEntry("types:max_width", 8));
}
/* Always warn about unused variables */
config.addConfig(ConfigEntry("typecheck:warnUnusedVars", true));
return config;
}
}

View File

@ -196,6 +196,25 @@ public class Compiler
this.typeChecker.beginCheck();
}
/**
* Returns the type checker instance of
* this compiler
*
* Returns: the type checker
* Throws:
* CompilerException if you have not
* called `doTypeCheck()` yet
*/
public TypeChecker getTypeChecker()
{
if(typeChecker is null)
{
throw new CompilerException(CompilerError.TYPECHECK_NOT_YET_PERFORMED);
}
return this.typeChecker;
}
/* Perform code emitting */
public void doEmit()
{
@ -255,7 +274,7 @@ public class Compiler
* sourceFile = the path to the file to open
* Returns: the source data
*/
private string gibFileData(string sourceFile)
public string gibFileData(string sourceFile)
{
File sourceFileFile;
sourceFileFile.open(sourceFile); /* TODO: Error handling with ANY file I/O */

View File

@ -209,6 +209,34 @@ public final class TypeChecker
gprintln("FUNCDEF DONE: "~to!(string)(functionBodyCodeQueues[funcData.name]));
}
/* Collect statistics */
doPostChecks();
}
/**
* These are just checks we run for the convenience
* of the user. They do not manipulate anything but
* rather provide statistics
*/
private void doPostChecks()
{
/**
* Find the variables which were declared but never used
*/
if(this.config.hasConfig("typecheck:warnUnusedVars") & this.config.getConfig("typecheck:warnUnusedVars").getBoolean())
{
Variable[] unusedVariables = getUnusedVariables();
gprintln("There are "~to!(string)(unusedVariables.length)~" unused variables");
if(unusedVariables.length)
{
foreach(Variable unusedVariable; unusedVariables)
{
// TODO: Get a nicer name, full path-based
gprintln("Variable '"~to!(string)(unusedVariable.getName())~"' is declared but never used");
}
}
}
}
@ -3257,6 +3285,51 @@ public final class TypeChecker
//assert()
}
/**
* Maps a given `Variable` to its reference
* count. This includes the declaration
* thereof.
*/
private uint[Variable] varRefCounts;
/**
* Increments the given variable's reference
* count
*
* Params:
* variable = the variable
*/
void touch(Variable variable)
{
// Create entry if not existing yet
if(variable !in this.varRefCounts)
{
this.varRefCounts[variable] = 0;
}
// Increment count
this.varRefCounts[variable]++;
}
/**
* Returns all variables which were declared
* but not used
*
* Returns: the array of variables
*/
public Variable[] getUnusedVariables()
{
Variable[] unused;
foreach(Variable variable; this.varRefCounts.keys())
{
if(!(this.varRefCounts[variable] > 1))
{
unused ~= variable;
}
}
return unused;
}
}
@ -3580,4 +3653,64 @@ unittest
typeChecker.beginCheck();
/* TODO: Actually test generated code queue */
}
/**
* Tests the unused variable detection mechanism
*
* Case: Positive (unused variables exist)
* Source file: source/tlang/testing/unused_vars.t
*/
unittest
{
// Dummy field out
File fileOutDummy;
import tlang.compiler.core;
string sourceFile = "source/tlang/testing/unused_vars.t";
Compiler compiler = new Compiler(gibFileData(sourceFile), fileOutDummy);
compiler.doLex();
compiler.doParse();
compiler.doTypeCheck();
TypeChecker tc = compiler.getTypeChecker();
/**
* There should be 1 unused variable and then
* it should be named `j`
*/
Variable[] unusedVars = tc.getUnusedVariables();
assert(unusedVars.length == 1);
Variable unusedVarActual = unusedVars[0];
Variable unusedVarExpected = cast(Variable)tc.getResolver().resolveBest(tc.getModule(), "j");
assert(unusedVarActual is unusedVarExpected);
}
/**
* Tests the unused variable detection mechanism
*
* Case: Negative (unused variables do NOT exist)
* Source file: source/tlang/testing/unused_vars_none.t
*/
unittest
{
// Dummy field out
File fileOutDummy;
import tlang.compiler.core;
string sourceFile = "source/tlang/testing/unused_vars_none.t";
Compiler compiler = new Compiler(gibFileData(sourceFile), fileOutDummy);
compiler.doLex();
compiler.doParse();
compiler.doTypeCheck();
TypeChecker tc = compiler.getTypeChecker();
/**
* There should be 0 unused variables
*/
Variable[] unusedVars = tc.getUnusedVariables();
assert(unusedVars.length == 0);
}

View File

@ -673,6 +673,9 @@ public class DNodeGenerator
/* Get the entity as a Variable */
Variable variable = cast(Variable)namedEntity;
/* Variable reference count must increase */
tc.touch(variable);
/* Pool the node */
VariableNode varDecNode = poolT!(VariableNode, Variable)(variable);
@ -980,6 +983,9 @@ public class DNodeGenerator
writeln("Hello");
writeln("VarType: "~to!(string)(variableType));
/* Add an entry to the reference counting map */
tc.touch(variable);
/* Basic type */
if(cast(Primitive)variableType)
{
@ -1082,6 +1088,9 @@ public class DNodeGenerator
Variable variable = cast(Variable)tc.getResolver().resolveBest(c, vAsStdAl.getVariableName());
assert(variable);
/* Assinging to a variable is usage, therefore increment the reference count */
tc.touch(variable);
/* Pool the variable */
DNode varDecDNode = pool(variable);
@ -1558,5 +1567,4 @@ public class DNodeGenerator
return classDNode;
}
}

View File

@ -0,0 +1,3 @@
module unused_vars;
int j;

View File

@ -0,0 +1,8 @@
module unused_vars;
int j;
void thing()
{
j = 1;
}

BIN
tlang

Binary file not shown.