Skip to content

Switchexpression#138

Open
glelouet wants to merge 12 commits into
phax:masterfrom
glelouet:switchexpression
Open

Switchexpression#138
glelouet wants to merge 12 commits into
phax:masterfrom
glelouet:switchexpression

Conversation

@glelouet
Copy link
Copy Markdown
Contributor

@glelouet glelouet commented May 9, 2026

change : java21 required
Based on master
Implementation of #136

Remaining:

  • fix java version. Can't work on 17

Done:

  • add Yield keyword
  • add SwitchExpression structure and package
  • Switchexpression can have null, default, static, or pattern caes.
  • all cases use the yield syntaxic sugar to add to the underlying block. Same for throw.
  • null is added at the end, then default. If they are equal, they should be merged (not working since expression .equals is based on ref)
  • this is required because switch expressions allow case null, default -> ; so switchexpression allows to add to both null and default, sw._null().andDefault()._throws(…)
  • static cases are the usual switch ones case 1, 2 ->
  • pattern cases allow a typed var case Integer i and therefore add var-specific addOn, yieldOn, throwOn to create the expression based on the matched variable i+1 or throw new Exception ("received"+i)
  • pattern allows guarding when
  • when printing (=state) the block, if only contains one yield then its internal expression is used instead ; same if only one throws, the throws is unnested.
  • specific enum variable "case" (translates it as a ref)
  • minor syntaxic sugar change to JCM to ref an enum value.

generator example :
https://github.com/glelouet/jcodemodel/blob/switchexpression/jcodemodeltests/src/main/java/com/helger/jcodemodel/tests/switchexpression/SwitchExpressionTestGen.java

result :
https://github.com/glelouet/jcodemodel/blob/switchexpression/jcodemodeltests/src/generated/javatest/com/helger/jcodemodel/tests/switchexpression/BasicSwitch.java

@glelouet
Copy link
Copy Markdown
Contributor Author

glelouet commented May 9, 2026

todo :

  1. java21 could not be required. if java.version<21 , then the PatternCases should be exported as if(o instance of Type name){}, all encapsulated in a callable block and yield should return instead ; then the rest of the witch should be converted to an old one (with null test if null case selected). A lot of work though.

  2. more tests , especially enums, both referenced and jdefined

@glelouet glelouet changed the title Switchexpression [WiP] Switchexpression May 9, 2026
@glelouet
Copy link
Copy Markdown
Contributor Author

I was making tests for enums

		JDefinedClass cl = root._class("ESwitch");
		JMethod m = cl.method(JMod.PUBLIC | JMod.STATIC, Number.class, "daysIn");
		JVar o = m.param(jcm.ref(Object.class), "o");
		JSwitchExpression sw = JExpr._switch(o);
		m.body()._return(sw);
		sw._default().andNull()._throws(jcm, UnsupportedOperationException.class);
		sw._case(JExpr.enumConstantRef(jcm.ref(EnumMonths.class), "JAN"))
				.yield(JExpr.lit(31));
		JDefinedClass ep = root._enum("EPeriod");
		JEnumConstant yearConstant = ep.enumConstant("YEAR");
		sw._case(yearConstant).yield(JExpr.lit(365));

generates

public class ESwitch {

    public static Number daysIn(Object o) {
        return switch (o) {
            case EnumMonths.JAN -> 
                31;
            case YEAR ->  // <- error
                365;
            case null, default -> {
                throw new UnsupportedOperationException();
            }
        }
        ;
    }
}

with the error

YEAR cannot be resolved to a variable

Should be EPeriod.YEAR instead, like the EnumMonths.JAN.

block. Trying to add enum testing but result is failed
@glelouet
Copy link
Copy Markdown
Contributor Author

adding a syntaxic sugar in JCodemodel

	///
	/// reference a existing enum value
	public @NonNull JEnumConstantRef ref(Enum<?> e) {
		return JExpr.enumConstantRef(ref(e.getDeclaringClass()), e.name());
	}

This allows to rewrite

JExpr.enumConstantRef(jcm.ref(EnumMonths.class),"JAN"); // old
jcm.ref(EnumMonths.JAN); // new

And is more resilient to refactoring

@glelouet
Copy link
Copy Markdown
Contributor Author

glelouet commented May 12, 2026

fixed aforementioned bug by adding in JSwitchExpression

	public JCaseStatic _case(@NonNull final JEnumConstant constantRef) {
		return _case(JExpr.enumConstantRef(constantRef.type(), constantRef.name()));
	}

with that the switch on enum makes correct code.

@glelouet
Copy link
Copy Markdown
Contributor Author

removing WiP, I think I've done my part, only issue I can think of is java version that requires a bump and prevent the CLI from working since it's java17 and java17 does not accept switch patterns :P

@glelouet glelouet changed the title [WiP] Switchexpression Switchexpression May 14, 2026
@glelouet
Copy link
Copy Markdown
Contributor Author

possible solutions for the java version problem

  1. remove support for java <21 . What's done in my code, not an acceptable solution
  2. make the generation of the switchexpression translate it into null-check, followed by old switch and list of instanceof ; this has to be inside a callable that is then called . This is very ugly . Example given below
  3. have the feature present whatever the java version, only filter tests on the java version. This means one src/test/java21 folder that is only activated on java.version≥21 , and test generators specifying their java required version in the annotation. I will try to do that now.

example of uglyness : same code in new and old styles

//length method of Object o
ret = switch(o){
  case String s -> switch(s.toLowerCase()){
    case "jan" -> 31;
    default->s.length();
  };
  case Collection<?> c ->c.size();
  case null, default -> 0;
};

would become

//length method of Object o
ret = new Callable<>(){
  public void call(){
    if(o==null) return 0;
    if(o instanceof String){
      String s = (String) o;
      return new Callable<>(){
        public void call(){
          switch(s.toLowerCase()){
            case "jan" : return 31; break;
            default : return s.length;
          }
        }
      }.call();
    }
    if(o instance of Collection) {
      Collection c = (Collection) o;
      return c.size();
    }
    return 0;
  }
}.call();

@glelouet
Copy link
Copy Markdown
Contributor Author

glelouet commented May 14, 2026

2026-05-14T12:12:17.8555234Z [ERROR] COMPILATION ERROR :

2026-05-14T12:12:17.8555948Z [INFO] -------------------------------------------------------------
2026-05-14T12:12:17.8597108Z [ERROR] /home/runner/work/jcodemodel/jcodemodel/jcodemodel/src/main/java/com/helger/jcodemodel/compile/DynamicClassLoader.java:[171,22] cannot find symbol
2026-05-14T12:12:17.8609490Z symbol: method of(java.net.URI,java.net.URLStreamHandler)
2026-05-14T12:12:17.8610573Z location: class java.net.URL

FFS !!!

public URL(String protocol, String host, int port, String file, URLStreamHandler handler) throws MalformedURLException
Is deprecated in java 21 so the build fails because calling deprecated method. So instead I call this method which does not exist in java17..

I don't even know if my hacks work, the project fails for stupid reasons beforehand !

@glelouet
Copy link
Copy Markdown
Contributor Author

raah

Java execution does not know of the "release" property. So I need to transmit it to the generator . But maven does not either ! So I can't activate a profile when java.version ≥ 21 because that's just not doable. I can activate a profile when java.version is set, or set TO 21 ; and I can activate a profile when JDK is ≥21 ; but the later will always be active on my java25 installation, even if I choose to build for java.version=17 !

so far I fixed the transmission, so that test generators that requires a higher java version than our java.version are skipped ; but I can't find how to make the java-21 specific classes ignored when running on java25 jdk with java.release=17 .

@glelouet
Copy link
Copy Markdown
Contributor Author

I made too many rambles, here is the issues explained in simplest way :

A method that I called is now deprecated in java 25. The replacement of that method does not exist in java 17, therefore I can't have same code for both releases without a deprecation call.

Since this project generates code depending on java features, and the last LTS is 25 https://en.wikipedia.org/wiki/Java_version_history#Java_SE_25_(LTS) , this raises the question of maintaining the whole project backward-compatible.

I think we should ditch 17 and 21 compatibility, only using the lat LTS and the LTS intermediate releases (so 25 and 26)

New java features can't be tested on an old java version.
I tried to allow to set the java release , but this ends with going down the rabbit hole of setting arbitrary variables just to workaround a root issue.
I think we should have master for last LTS + newer versions (so 25, 26) and dedicated branch for newer version (so 26)

So again, no more java17 as this is weights us down.

Another possibility would be, to have dedicated submodules, in our case /java21 that depends on the original project (/jcodemodel) and add release-bound feature classes in them ; but that would still be a lot of hassle and a feeding ground for bugs, plus would force the user to be aware of internal changes

@glelouet glelouet mentioned this pull request May 14, 2026
12 tasks
@glelouet
Copy link
Copy Markdown
Contributor Author

glelouet commented May 20, 2026

ok so here is what I'm gonna do : Keep that branch on my side, close the PR, make a new one with only java17 switch expression ; make another branch and PR with java25 switchpattern.

The new branch will be made with future switchpattern in mind.

What about it @phax ?

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