Skip to content

Enhance support for switch statements - #99

Draft
kjw142857 wants to merge 2 commits into
mainfrom
switch-statements
Draft

Enhance support for switch statements#99
kjw142857 wants to merge 2 commits into
mainfrom
switch-statements

Conversation

@kjw142857

Copy link
Copy Markdown
Contributor

This PR aims to build on existing support for switch statements by completing the type of selector accepted, in particular narrowing it down to String, integral types or enum types.

@kjw142857 kjw142857 self-assigned this Aug 10, 2026
@github-actions

Copy link
Copy Markdown

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements
71.79% (-0.48% 🔻)
7493/10438
🔴 Branches
58.98% (-0.14% 🔻)
2526/4283
🟡 Functions
69.21% (+0.18% 🔼)
1331/1923
🟡 Lines
72.76% (-0.36% 🔻)
7047/9685
Show files with reduced coverage 🔻
St.
File Statements Branches Functions Lines
🟡
... / index.ts
66.37% (-4.19% 🔻)
46.18% (-1.13% 🔻)
76.67% (-15% 🔻)
74.77% (-4.69% 🔻)
🟡
... / prechecks.ts
60.12% (-22.98% 🔻)
48.72% (-9.35% 🔻)
93.33% (-6.67% 🔻)
67.18% (-21.16% 🔻)
🟡
... / code-generator.ts
65.03% (-0.64% 🔻)
59.57% (-0.8% 🔻)
65.43%
65.8% (-0.67% 🔻)

Test suite run success

1138 tests passing in 64 suites.

Report generated by 🧪jest coverage report action from 89ccf30

@kjw142857

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Enum switch support

Layer / File(s) Summary
Enum type registration and members
src/types/types/classes.ts, src/types/checker/environment.ts, src/types/checker/prechecks.ts
Enum declarations register as EnumClass types. Enum constants, fields, constructors, methods, nested declarations, and the built-in Enum parent are processed.
Enum and switch type checking
src/types/checker/index.ts, src/types/checker/statements.ts, src/types/checker/__tests__/switchStatements.test.ts
The checker validates enum declarations and switch labels. String and enum selectors are accepted, Boolean selectors are rejected, and incompatible enum cases produce errors.
Enum switch code generation
src/compiler/compiler-utils.ts, src/compiler/code-generator.ts
Enum classes emit ACC_ENUM. Enum switch expressions call Enum.ordinal() before integer switch generation. Unsupported-type diagnostics include enum support.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CompilationUnit
  participant prechecks as prechecks.ts
  participant Checker
  participant CodeGenerator
  CompilationUnit->>prechecks: discover and register enum declarations
  prechecks->>Checker: validate enum members and methods
  Checker->>Checker: validate enum switch selectors and labels
  Checker->>CodeGenerator: compile validated enum switch
  CodeGenerator->>CodeGenerator: call Enum.ordinal()
  CodeGenerator->>CodeGenerator: generate integer switch dispatch
Loading

Poem

A rabbit checks each enum name,
Then hops through cases in a row.
Ordinal numbers join the game,
While typed declarations grow.
ACC_ENUM flags sparkle bright—
Switches compile just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: enhanced switch-statement support, including selector type restrictions and enum support.
Description check ✅ Passed The description directly explains the switch selector type changes and matches the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch switch-statements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/compiler/code-generator.ts (1)

1407-1424: 🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Resolve enum case labels before generating integer switch keys

case RED is an ExpressionName, not a Literal. Line 1416 therefore throws a TypeError before generating switch bytecode. Resolve each enum case constant to its ordinal, matching the selector's Enum.ordinal() conversion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/code-generator.ts` around lines 1407 - 1424, The integer switch
generation in the case-label processing within the surrounding code-generator
method assumes every CaseLabel expression is a Literal; update it to also
resolve enum ExpressionName constants to their ordinal values, matching the
selector’s Enum.ordinal() conversion, before adding values to caseValues and
caseLabelMap. Preserve literal handling for non-enum cases and default-label
behavior.
🧹 Nitpick comments (5)
src/types/checker/environment.ts (1)

44-46: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider adding the Enum base members that the code generator depends on.

The global Enum entry is an empty ClassType. It declares no ordinal(), name(), or compareTo(...) members. src/compiler/code-generator.ts emits INVOKEVIRTUAL java/lang/Enum.ordinal()I for enum switch selectors, so the runtime contract assumes those members exist. A source program that calls selector.ordinal() will fail type checking with CannotFindSymbolError, even though the compiler can emit the call.

Adding at least ordinal() returning int and name() returning String keeps the type environment consistent with the emitted bytecode.

Using ClassType rather than EnumClass for the base is correct here, because checkSwitchExpression must not accept the abstract base as a selector.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/checker/environment.ts` around lines 44 - 46, The global Enum entry
in the type environment is missing members required by type checking and
generated bytecode. Update the Enum ClassType declaration to add ordinal()
returning int and name() returning String, preserving it as ClassType so
checkSwitchExpression does not accept the abstract base as a selector.
src/compiler/code-generator.ts (1)

1379-1398: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the try block and correct the maxStack adjustment.

Two points on this normalization block.

The try at line 1383 wraps both the queryClass lookup and the bytecode emission. queryClass throws SymbolNotFoundError for an unresolved name, which is the case this code intends to tolerate. The current form also swallows any failure from indexMethodrefInfo. Wrap only the lookup, or check for the class before emitting.

Line 1393 sets maxStack to exprStackSize + 1. ordinal() pops the objectref and pushes an int, so the net stack change is zero and the peak stays at exprStackSize. Over-reserving is safe for the verifier, but the extra slot is unnecessary and the expression suggests a growth that does not occur.

♻️ Proposed refactor
     let _resultType = resultType
     if (_resultType && _resultType.startsWith('L') && _resultType !== 'Ljava/lang/String;') {
       const clean = _resultType.replace(/^L|;$/g, '')
+      let classInfo
       try {
-        const classInfo = cg.symbolTable.queryClass(clean)
-        if (classInfo.accessFlags & ACCESS_FLAGS.ACC_ENUM) {
-          // call java.lang.Enum.ordinal() (returns int)
-          cg.code.push(
-            OPCODE.INVOKEVIRTUAL,
-            0,
-            cg.constantPoolManager.indexMethodrefInfo('java/lang/Enum', 'ordinal', '()I')
-          )
-          _resultType = 'I'
-          maxStack = Math.max(maxStack, exprStackSize + 1)
-        }
-      } catch (e) {
-        // ignore: not a known class
+        classInfo = cg.symbolTable.queryClass(clean)
+      } catch {
+        classInfo = undefined // not a known class
+      }
+      if (classInfo && classInfo.accessFlags & ACCESS_FLAGS.ACC_ENUM) {
+        // call java.lang.Enum.ordinal() (returns int)
+        cg.code.push(
+          OPCODE.INVOKEVIRTUAL,
+          0,
+          cg.constantPoolManager.indexMethodrefInfo('java/lang/Enum', 'ordinal', '()I')
+        )
+        _resultType = 'I'
       }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/code-generator.ts` around lines 1379 - 1398, Narrow the
try/catch in the enum normalization block around cg.symbolTable.queryClass so
only unresolved-class lookup failures are ignored; let indexMethodrefInfo and
bytecode emission errors propagate. When emitting Enum.ordinal(), update
maxStack using exprStackSize rather than exprStackSize + 1, since the invocation
has zero net stack growth.
src/types/checker/index.ts (2)

585-666: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract the shared class-body checking logic instead of duplicating the NormalClassDeclaration branch.

Lines 585-666 duplicate lines 487-583 almost verbatim. Only the declaration list source differs: node.classBody.classBodyDeclarations becomes bodyDecls. The frame setup, the constructor index arithmetic, the field initializer check, the overload index computation, and the counter updates are identical.

Two copies must now stay in sync. A fix applied to one branch will silently miss the other.

Extract a helper that takes classType, the declaration list, and the frame, then call it from both branches.

The as any casts at lines 622, 639, 642, and 656 are also avoidable. The surrounding switch (bodyDeclaration.kind) already narrows the node type, and the equivalent class branch needs no casts. If bodyDecls is typed as any[], type the enum body declaration list properly so the narrowing works.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/checker/index.ts` around lines 585 - 666, Extract the duplicated
class-body checking flow from the NormalClassDeclaration and EnumDeclaration
branches into a shared helper accepting classType, declaration list, and frame,
preserving constructor indexing, field initializer checks, method overload
resolution, and declaration counters. Invoke the helper from both branches, and
type the enum declaration list so switch narrowing removes the unnecessary as
any casts in the enum handling.

727-740: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the typed caseConstants field for checker switch labels.

SwitchLabel declares only caseConstants, and the checker AST extractor emits that field. Remove singular-property probing and as any casts. Narrow to the caseConstants variant and iterate over switchLabel.caseConstants. The separate src/ast/astExtractor/statement-extractor.ts model is not used by this checker.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/checker/index.ts` around lines 727 - 740, Update the switch-label
handling in the type-checker branch to use only the typed
SwitchLabel.caseConstants field. Remove the singular caseConstant probing and
all any casts, narrow to the caseConstants variant, and iterate directly over
switchLabel.caseConstants while preserving the existing type-checking and error
behavior.
src/types/checker/prechecks.ts (1)

128-159: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Enum constant and method registration look correct.

Registering each enum constant as a field whose type is the enum class matches Java semantics. The constructor and method handling mirrors the NormalClassDeclaration path.

One consistency note: this branch returns on the first error, while the NormalClassDeclaration path accumulates errors and reports them together. Accumulating here would report all enum body problems in one pass.

Also applies to: 173-185

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/checker/prechecks.ts` around lines 128 - 159, The EnumDeclaration
processing should accumulate errors from enum constants, constructors, and
methods instead of returning on the first failure. Update the enum registration
loops and createMethodLocal handling to collect TypeCheckerError instances,
continue processing remaining declarations, and return the combined errors after
the enum body has been processed, matching the NormalClassDeclaration path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/compiler/compiler-utils.ts`:
- Around line 9-10: Remove the unreachable enum entry from the class-modifier
mapping, or implement enum declaration parsing separately from
NormalClassDeclaration and assign ACCESS_FLAGS.ACC_ENUM from the declaration
kind rather than classModifier. Ensure ClassModifier remains limited to
supported class modifiers.

In `@src/types/checker/__tests__/switchStatements.test.ts`:
- Around line 76-100: Update the switch statement tests to use Java-valid
unqualified enum labels: change the positive case to case RED and retain the
incompatible case with an unqualified constant from Other, then add coverage for
resolving selector enum constants in case-label scope. Also add a test declaring
an enum at top level to exercise the registration path in prechecks alongside
the existing method-local declarations.

In `@src/types/checker/prechecks.ts`:
- Around line 223-234: The OrdinaryCompilationUnit branch of addClassParents
must traverse nested EnumDeclaration nodes, including enums declared inside
method bodies, before or alongside topLevelClassOrInterfaceDeclarations. Reuse
the existing nested-enum traversal pattern from addClasses or addClassMethods,
and ensure each discovered enum receives the Enum ClassType parent through the
existing parent-assignment logic.
- Around line 18-42: Extract the nested-enum traversal from registerNestedEnums
into one shared helper that descends into each top-level declaration’s children
without visiting the top-level declaration itself. Reuse this helper in the
declaration pass around registerNestedEnums and the pass containing
processNestedEnums, removing their local walkers and duplicate enum processing.
Also invoke the shared helper in the OrdinaryCompilationUnit branch of
addClassParents so nested enums receive the Enum parent. Apply these changes in
src/types/checker/prechecks.ts at lines 18-42, 91-107, and 223-234.
- Around line 160-172: Update the FieldDeclaration handling to convert
bodyNode.unannType with unannTypeToString before passing it to frame.getType,
while preserving the existing fieldType fallback; import unannTypeToString from
its defining module so enum field processing supplies the string expected by
getType.

In `@src/types/checker/statements.ts`:
- Around line 31-34: Update the selector validation around
isPrimitiveIntegralType, isPrimitiveLongType, isStringType, and EnumClass to
also accept boxed integral types Character, Byte, Short, and Integer, reusing
the existing type predicates or classes. Preserve rejection of Boolean and Long,
and add coverage for at least one boxed integral selector in
switchStatements.test.ts.

---

Outside diff comments:
In `@src/compiler/code-generator.ts`:
- Around line 1407-1424: The integer switch generation in the case-label
processing within the surrounding code-generator method assumes every CaseLabel
expression is a Literal; update it to also resolve enum ExpressionName constants
to their ordinal values, matching the selector’s Enum.ordinal() conversion,
before adding values to caseValues and caseLabelMap. Preserve literal handling
for non-enum cases and default-label behavior.

---

Nitpick comments:
In `@src/compiler/code-generator.ts`:
- Around line 1379-1398: Narrow the try/catch in the enum normalization block
around cg.symbolTable.queryClass so only unresolved-class lookup failures are
ignored; let indexMethodrefInfo and bytecode emission errors propagate. When
emitting Enum.ordinal(), update maxStack using exprStackSize rather than
exprStackSize + 1, since the invocation has zero net stack growth.

In `@src/types/checker/environment.ts`:
- Around line 44-46: The global Enum entry in the type environment is missing
members required by type checking and generated bytecode. Update the Enum
ClassType declaration to add ordinal() returning int and name() returning
String, preserving it as ClassType so checkSwitchExpression does not accept the
abstract base as a selector.

In `@src/types/checker/index.ts`:
- Around line 585-666: Extract the duplicated class-body checking flow from the
NormalClassDeclaration and EnumDeclaration branches into a shared helper
accepting classType, declaration list, and frame, preserving constructor
indexing, field initializer checks, method overload resolution, and declaration
counters. Invoke the helper from both branches, and type the enum declaration
list so switch narrowing removes the unnecessary as any casts in the enum
handling.
- Around line 727-740: Update the switch-label handling in the type-checker
branch to use only the typed SwitchLabel.caseConstants field. Remove the
singular caseConstant probing and all any casts, narrow to the caseConstants
variant, and iterate directly over switchLabel.caseConstants while preserving
the existing type-checking and error behavior.

In `@src/types/checker/prechecks.ts`:
- Around line 128-159: The EnumDeclaration processing should accumulate errors
from enum constants, constructors, and methods instead of returning on the first
failure. Update the enum registration loops and createMethodLocal handling to
collect TypeCheckerError instances, continue processing remaining declarations,
and return the combined errors after the enum body has been processed, matching
the NormalClassDeclaration path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 476c8050-48ea-410f-8baa-c3a0e76dd316

📥 Commits

Reviewing files that changed from the base of the PR and between 8ecede1 and 89ccf30.

📒 Files selected for processing (8)
  • src/compiler/code-generator.ts
  • src/compiler/compiler-utils.ts
  • src/types/checker/__tests__/switchStatements.test.ts
  • src/types/checker/environment.ts
  • src/types/checker/index.ts
  • src/types/checker/prechecks.ts
  • src/types/checker/statements.ts
  • src/types/types/classes.ts

Comment on lines +9 to +10
['abstract', ACCESS_FLAGS.ACC_ABSTRACT],
['enum', ACCESS_FLAGS.ACC_ENUM]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm that 'enum' reaches classModifier and that ACC_ENUM can be set.
set -euo pipefail

echo '=== ClassModifier type definition ==='
rg -nP -C 4 '\bClassModifier\b' --type=ts src/ast/types

echo '=== Where classModifier is populated ==='
rg -nP -C 5 'classModifier' --type=ts src

echo '=== Any enum handling in the AST extractor ==='
rg -nP -C 5 -i 'enumdeclaration|enumconstant' --type=ts src/ast

echo '=== ACC_ENUM definition and all uses ==='
rg -nP -C 3 'ACC_ENUM' --type=ts src

Repository: source-academy/java-slang

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== AST class types ==='
cat -n src/ast/types/classes.ts

echo '=== Enum-related source symbols ==='
rg -n -i --glob '*.ts' '\benum\b|EnumDeclaration|enumConstant' src --glob '!**/__tests__/**' | head -200

echo '=== Class declaration extraction and parser definitions ==='
rg -n -i --glob '*.ts' 'NormalClassDeclaration|classModifier|classDeclaration' src/parser src/ast src 2>/dev/null | head -240

Repository: source-academy/java-slang

Length of output: 30302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Compiler inputs and class extraction ==='
rg -n -C 6 --glob '*.ts' 'classModifier|ClassDeclaration|NormalClassDeclaration|generateClassAccessFlags' src/compiler src/ast

echo '=== Legacy compiler grammar around class declarations ==='
sed -n '130,275p' src/compiler/grammar.ts

echo '=== Legacy class extractor ==='
cat -n src/ast/astExtractor/class-extractor.ts

echo '=== Compiler entry point ==='
sed -n '1,140p' src/compiler/compiler.ts

echo '=== New AST enum declaration and class declaration types ==='
sed -n '230,270p' src/types/ast/specificationTypes.ts
sed -n '460,485p' src/types/ast/specificationTypes.ts
sed -n '360,390p' src/types/ast/extractor.ts
sed -n '690,715p' src/types/ast/extractor.ts

Repository: source-academy/java-slang

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Legacy grammar: class modifiers and enum token ==='
sed -n '160,190p' src/compiler/grammar.ts
sed -n '490,525p' src/compiler/grammar.ts

echo '=== Legacy parser entry points ==='
fd -t f -e ts . src/ast src/compiler | sort | grep -E 'parser|grammar|extractor'
rg -n -C 8 'from .*compiler|grammar|parse\(' src/compiler src/ast --glob '*.ts' | head -180

echo '=== Class modifier implementation ==='
rg -n -C 10 'ClassModifier|possibleModifiers|classModifier\(ctx' src/compiler/grammar.ts src/ast/astExtractor/class-extractor.ts src/ast/parser* 2>/dev/null

Repository: source-academy/java-slang

Length of output: 21495


Model enum declarations separately from class modifiers

ClassModifier and ClassModifier* do not include enum. The compiler therefore cannot place enum in classModifier; its grammar does not parse enum declarations as NormalClassDeclaration. This entry cannot set ACC_ENUM. Add enum declaration support and set the flag from the declaration kind, or remove this unreachable mapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/compiler/compiler-utils.ts` around lines 9 - 10, Remove the unreachable
enum entry from the class-modifier mapping, or implement enum declaration
parsing separately from NormalClassDeclaration and assign ACCESS_FLAGS.ACC_ENUM
from the declaration kind rather than classModifier. Ensure ClassModifier
remains limited to supported class modifiers.

Comment on lines +76 to +100
},
{
input: `
enum Color { RED, BLUE }
Color selector = Color.RED;
switch(selector) {
case Color.RED: {
selector = Color.BLUE;
}
default: {}
}
`,
result: { type: null, errors: [] }
},
{
input: `
enum Color { RED, BLUE }
enum Other { X }
Color selector = Color.RED;
switch(selector) {
case Other.X: {}
default: {}
}
`,
result: { type: null, errors: [new IncompatibleTypesError()] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

These tests use qualified enum constants in case labels, which Java does not permit.

Java requires the unqualified constant name in an enum switch label. case Color.RED: is a compile error in javac; the legal form is case RED:. Both new tests use the qualified form.

Two consequences:

  1. The positive test at lines 77-89 asserts that an invalid Java program type checks successfully.
  2. The negative test at lines 90-100 passes for the wrong reason. Real javac rejects case Other.X: because it is qualified, not because of a type mismatch.

The unqualified form is the one that matters, and it is untested. Resolving a bare RED requires the checker to bring the selector enum's constants into the case-label scope. Nothing in src/types/checker/index.ts lines 727-740 does that, so case RED: would likely fail with CannotFindSymbolError.

Add a test using case RED: to confirm the behavior.

Both tests also declare the enum inside the main method body, because createProgram inserts the statement there. No test declares a top-level enum, so the top-level registration path in src/types/checker/prechecks.ts is uncovered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/checker/__tests__/switchStatements.test.ts` around lines 76 - 100,
Update the switch statement tests to use Java-valid unqualified enum labels:
change the positive case to case RED and retain the incompatible case with an
unqualified constant from Other, then add coverage for resolving selector enum
constants in case-label scope. Also add a test declaring an enum at top level to
exercise the registration path in prechecks alongside the existing method-local
declarations.

Comment on lines +18 to +42

// Register any nested enum declarations found anywhere in the compilation unit
const registerNestedEnums = (obj: any) => {
if (!obj || typeof obj !== 'object') return
if (Array.isArray(obj)) {
obj.forEach(registerNestedEnums)
return
}
if (obj.kind === 'EnumDeclaration') {
try {
const enumType = new EnumClass(obj.typeIdentifier.identifier)
const err = frame.setType(obj.typeIdentifier.identifier, enumType, obj.typeIdentifier.location)
if (err instanceof Error) {
// duplicate class — add as error
typeCheckErrors.push(new DuplicateClassError(obj.location))
}
} catch (e) {
// ignore
}
return
}
Object.keys(obj).forEach(k => registerNestedEnums(obj[k]))
}
node.topLevelClassOrInterfaceDeclarations.forEach(registerNestedEnums)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Nested enum discovery is duplicated, applied to already-visited nodes, and missing from one pass. All three declaration passes need the same nested-enum traversal, but each handles it differently: two passes define their own untyped any walker and run it over topLevelClassOrInterfaceDeclarations, which the recursive addClasses / addClassMethods calls already visited, and the third pass has no walker at all. The shared fix is one traversal helper that descends below the top-level declarations and is reused by all three passes.

  • src/types/checker/prechecks.ts#L18-L42: change registerNestedEnums so it starts below each top-level declaration instead of visiting the declaration itself, which stops the second frame.setType call and the spurious DuplicateClassError. Extract this traversal into a shared helper.
  • src/types/checker/prechecks.ts#L91-L107: replace the local processNestedEnums walker with the shared helper, so a top-level EnumDeclaration is no longer passed to addClassMethods twice and enum constants are not re-added through classType.addField.
  • src/types/checker/prechecks.ts#L223-L234: add the shared helper to the OrdinaryCompilationUnit case of addClassParents, so nested enums also reach this branch and receive the Enum parent instead of keeping the default ObjectClass.
📍 Affects 1 file
  • src/types/checker/prechecks.ts#L18-L42 (this comment)
  • src/types/checker/prechecks.ts#L91-L107
  • src/types/checker/prechecks.ts#L223-L234
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/checker/prechecks.ts` around lines 18 - 42, Extract the nested-enum
traversal from registerNestedEnums into one shared helper that descends into
each top-level declaration’s children without visiting the top-level declaration
itself. Reuse this helper in the declaration pass around registerNestedEnums and
the pass containing processNestedEnums, removing their local walkers and
duplicate enum processing. Also invoke the shared helper in the
OrdinaryCompilationUnit branch of addClassParents so nested enums receive the
Enum parent. Apply these changes in src/types/checker/prechecks.ts at lines
18-42, 91-107, and 223-234.

Comment on lines +160 to +172
case 'FieldDeclaration': {
const fieldType = frame.getType(
(bodyNode as any).unannType ? (bodyNode as any).unannType : (bodyNode as any).fieldType,
bodyNode.location
)
if (fieldType instanceof TypeCheckerError) return newResult(null, [fieldType])
for (const declarator of (bodyNode as any).variableDeclaratorList.variableDeclarators) {
const fieldIdentifier = declarator.variableDeclaratorId.identifier
const error = classType.addField(fieldIdentifier.identifier, fieldType, fieldIdentifier.location)
if (error instanceof TypeCheckerError) return newResult(null, [error])
}
break
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm FieldDeclaration AST shape and how frame.getType is called elsewhere.
set -euo pipefail

echo '=== FieldDeclaration type definition ==='
ast-grep run --pattern 'export type FieldDeclaration = { $$$ }' --lang typescript src/types/ast/specificationTypes.ts

echo '=== createClassFieldsAndMethods: how class fields resolve their type ==='
fd -t f 'classFactory|createClassFieldsAndMethods' src || true
rg -nP -C 6 'createClassFieldsAndMethods' --type=ts src

echo '=== All frame.getType call sites ==='
rg -nP -C 1 '\bframe\.getType\s*\(' --type=ts src

Repository: source-academy/java-slang

Length of output: 10573


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Enum field-processing implementation ==='
sed -n '128,180p' src/types/checker/prechecks.ts

echo '=== Type lookup contract ==='
sed -n '60,95p' src/types/checker/environment.ts

echo '=== Existing class-field implementation ==='
sed -n '1,75p' src/types/typeFactories/classFactory.ts

echo '=== Type-name conversion ==='
rg -n -C 8 'export .*unannTypeToString|function unannTypeToString|const unannTypeToString' src/types/ast

Repository: source-academy/java-slang

Length of output: 8363


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Array-type predicate used before the type-map lookup ==='
rg -n -C 6 'export .*isArrayType|function isArrayType|const isArrayType' src/types

echo '=== prechecks imports and type utility signature ==='
sed -n '1,12p' src/types/checker/prechecks.ts
sed -n '65,105p' src/types/ast/utils.ts

Repository: source-academy/java-slang

Length of output: 2226


Convert enum field types before lookup

FieldDeclaration.unannType is an AST node. frame.getType expects a string and calls isArrayType, which invokes .trim() on that argument. This causes a runtime error while processing enum fields. Use unannTypeToString(bodyNode.unannType) and import the helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/checker/prechecks.ts` around lines 160 - 172, Update the
FieldDeclaration handling to convert bodyNode.unannType with unannTypeToString
before passing it to frame.getType, while preserving the existing fieldType
fallback; import unannTypeToString from its defining module so enum field
processing supplies the string expected by getType.

Comment on lines +223 to +234
case 'EnumDeclaration': {
const classType = frame.getType(node.typeIdentifier.identifier, node.typeIdentifier.location)
if (classType instanceof Error) return newResult(null, [classType])
if (!(classType instanceof ClassType)) throw new Error('enum type should be a ClassImpl')

// Enums implicitly extend java.lang.Enum (represented here as 'Enum' in the type environment)
const enumBase = frame.getType('Enum', node.typeIdentifier.location)
if (enumBase instanceof Error) return newResult(null, [enumBase])
if (!(enumBase instanceof ClassType)) throw new Error('Enum base should be a ClassImpl')
classType.setParentClass(enumBase)
return newResult(classType)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

addClassParents has no nested-enum walker, so nested enums never receive the Enum parent.

addClasses (lines 19-41) and addClassMethods (lines 92-106) both walk the AST to find nested EnumDeclaration nodes. The OrdinaryCompilationUnit case of addClassParents at lines 193-198 does not. It only maps over topLevelClassOrInterfaceDeclarations.

An enum declared inside a method body is therefore registered and populated, but keeps the default ObjectClass parent. The tests in src/types/checker/__tests__/switchStatements.test.ts declare enums inside main, so they exercise exactly this path.

The parent assignment matters for member lookup: ClassType.accessField and ClassType.accessMethod delegate to this._parent, so a nested enum cannot resolve any inherited Enum member.

Apply the same nested traversal here, or centralize the traversal once and reuse it in all three passes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/checker/prechecks.ts` around lines 223 - 234, The
OrdinaryCompilationUnit branch of addClassParents must traverse nested
EnumDeclaration nodes, including enums declared inside method bodies, before or
alongside topLevelClassOrInterfaceDeclarations. Reuse the existing nested-enum
traversal pattern from addClasses or addClassMethods, and ensure each discovered
enum receives the Enum ClassType parent through the existing parent-assignment
logic.

Comment on lines 31 to 34
if (isPrimitiveIntegralType(expressionType) && !isPrimitiveLongType(expressionType)) return null
if (isReferenceType(expressionType)) return null
if (isStringType(expressionType)) return null
if (expressionType instanceof EnumClass) return null
return new SelectorTypeNotAllowedError(location)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Boxed integral selector types are now rejected, but Java allows them.

The previous implementation accepted any reference type. This version accepts only String and EnumClass. Boxed integral types therefore now produce SelectorTypeNotAllowedError.

JLS 14.11 permits a switch selector of type char, byte, short, int, the boxed types Character, Byte, Short, Integer, String, or an enum type. Rejecting Boolean and Long is correct. Rejecting Integer is not.

The new test at src/types/checker/__tests__/switchStatements.test.ts lines 42-50 covers the Boolean rejection but not a boxed integral selector, so the regression is untested.

🐛 Proposed fix to accept boxed integral selectors
 export const checkSwitchExpression = (
   expressionType: Type,
   location: Location
 ): null | TypeCheckerError => {
   if (isPrimitiveIntegralType(expressionType) && !isPrimitiveLongType(expressionType)) return null
+  if (isReferenceIntegralType(expressionType) && !isReferenceLongType(expressionType)) return null
   if (isStringType(expressionType)) return null
   if (expressionType instanceof EnumClass) return null
   return new SelectorTypeNotAllowedError(location)
 }

Run the following script to check which boxed-type predicates already exist:

#!/bin/bash
# Description: List existing type predicates and confirm boxed integral coverage.
set -euo pipefail

rg -nP -C 2 'export const is\w*Type\s*=' --type=ts src/types/types/utils.ts
echo '=== Reference numeric type classes ==='
rg -nP -C 2 'class (Integer|Character|Byte|Short|Long|Boolean)\b' --type=ts src/types/types
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/checker/statements.ts` around lines 31 - 34, Update the selector
validation around isPrimitiveIntegralType, isPrimitiveLongType, isStringType,
and EnumClass to also accept boxed integral types Character, Byte, Short, and
Integer, reusing the existing type predicates or classes. Preserve rejection of
Boolean and Long, and add coverage for at least one boxed integral selector in
switchStatements.test.ts.

@martin-henz
martin-henz requested a review from kellywsq03 August 11, 2026 06:54
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.

1 participant