Skip to content

Track if mutable variable is ever reassigned (#595)#605

Open
alexzzzs wants to merge 1 commit into
kengorab:masterfrom
alexzzzs:feature/var-reassignment-tracking
Open

Track if mutable variable is ever reassigned (#595)#605
alexzzzs wants to merge 1 commit into
kengorab:masterfrom
alexzzzs:feature/var-reassignment-tracking

Conversation

@alexzzzs

Copy link
Copy Markdown

Overview
Implements issue #595 by tracking whether mutable variables are ever reassigned, providing LSP warnings and compilation optimizations.

Changes

Typechecker (projects/compiler/src/typechecker.abra)

  • Added isReassigned: Bool = false field to Variable type
  • Track reassignment in _typecheckAssignment

LSP (projects/lsp/src/language_service.abra)

  • Added warnings for mutable variables that are never reassigned
  • Suggests using val instead of var when appropriate

IR Generation (projects/compiler/src/ir.abra)

  • Treat unreassigned mutable variables as immutable for performance
  • Avoids heap allocation for captured variables
  • Enables better register allocation

Benefits

  • Developer Experience: LSP warnings help write more idiomatic code
  • Performance: Compile-time optimizations for variables that don't need mutability

Example

1 // Generates warning: "Variable 'x' is declared as mutable but never reassigned..."
2 var x = 10
3
4 // No warning - variable is reassigned
5 var y = 20
6 y = 30

Testing

  • Added test cases in projects/compiler/test/typechecker/bindingdecl/var_reassignment.abra

@kengorab

kengorab commented Sep 6, 2025

Copy link
Copy Markdown
Owner

Hey, first of all thank you for the contribution. I never expected that!
I'm typically hesitant to accept contributions in this project, just because it's still such a WIP state and things change (read: break) all the time.

As for the change itself, I'm a bit concerned about the _checkNestedScopes function since it essentially revisits each scope of each function in the module. One way I was thinking about this problem was to expand upon the existing identsByLine field in the Typechecker and make it a larger LspDiagnostics type, maybe with the shape:

type LspDiagnostics {
  identsByLine: Map<Int, (Int, Int, IdentifierMeta)[]> // existing identsByLine information goes here
  unreassignedVars: (String, Position)[] // (name, (line, col))
}

Then, whenever the typechecker ends a scope, it could check which mutable variables had not been reassigned to and push them into lspDiagnostics.unreassignedVars (if lspMode == true of course). This has the advantage of keeping track of this useful information in a single pass while the information is accessible anyway, as opposed to having to re-traverse every scope and child scope after the fact. And, since every if-else block, for-/while-loop, match arm, and try-else block each create a child scope (which themselves could also contain child scopes, eg nested ifs), this could potentially be a very expensive procedure.

If you have any thoughts on this approach, or want to submit a pull request to take a shot at the idea outlined above, I'll be happy to check it out. But I do want to caution against any expectations of stability in this project, as I change things day-to-day sometimes and you may get merge conflicts.

@alexzzzs

alexzzzs commented Sep 7, 2025

Copy link
Copy Markdown
Author

Hi!

First of all I do really like your project so I really wanted to contribute and help out!

I also appreciate the feedback on the implementation! I really appreciate you taking the time to review the contribution :)

I did just realize you're very right about the performance concerns with the _checkNestedScopes approach. I understand that revisiting each scope and function after typechecking could be expensive, especially in larger codebases with deeply nested scopes.

I really like your suggestion about expanding the existing identsByLine field into a more comprehensive LspDiagnostics type. This approach would indeed be much more efficient since it would track unreassigned variables during the single-pass typechecking process rather than requiring a separate traversal afterward.

I may be a bit out of my element here, but here's how I'm thinking we could implement your suggestion:

Create a new LspDiagnostics type that extends the current functionality:

type LspDiagnostics {
  identsByLine: Map<Int, (Int, Int, IdentifierMeta)[]> // existing functionality
  unreassignedVars: (Variable, Position)[] // new tracking field
}

Modify the typechecker to populate unreassignedVars during typechecking:

  • Add mutable variables to the tracking list when they're declared
  • Remove them from the list when they're reassigned
  • This way, we only need to check the list at the end, rather than traversing all scopes

Update the LSP to use this information directly instead of the current _checkNestedScopes approach.

I think this would improve the performance while still providing the same developer experience benefits. I think this approach aligns well with the existing architecture and would be much more efficient.

I'd also like to hear your thoughts about this too!

Thanks :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants