Skip to content

[Draft] Elixir 1.19 Support#3776

Draft
KarimElsayad247 wants to merge 37 commits intoKronicDeth:mainfrom
KarimElsayad247:feature/elixir-119
Draft

[Draft] Elixir 1.19 Support#3776
KarimElsayad247 wants to merge 37 commits intoKronicDeth:mainfrom
KarimElsayad247:feature/elixir-119

Conversation

@KarimElsayad247
Copy link

@KarimElsayad247 KarimElsayad247 commented Feb 2, 2026

I'm building upon @joshuataylor 's branch https://github.com/joshuataylor/intellij-elixir/tree/feature/elixir-119

This is still a draft/WIP

The goal (for me) is to resolve all failing tests

Number of failing tests when compiling on 1.19 without any fixes:

image

@KarimElsayad247 KarimElsayad247 changed the title [Draft] Elixir 1.19 [Draft] Elixir 1.19 Support Feb 2, 2026
@KarimElsayad247 KarimElsayad247 marked this pull request as draft February 2, 2026 16:47
@KarimElsayad247
Copy link
Author

Supporting :from_brackets quoted-form metadata which was introduced in 1.15 resolved 40 extra tests

Failed tests are down to 253 from 293

image

@KarimElsayad247
Copy link
Author

Alright I missed a whole lot of bracketed operations

It was introduced with Elixir 1.14

Tests:
  3574 passing (1m 17s)
  253 failing
@KarimElsayad247
Copy link
Author

Aight fixed and force-pushed. Failing tests down to 166, meaning 127 tests failed due to from_brackets missing

image

Next on my list is from_interpolation which was introduced in 1.16

It was introduced with Elixir 1.16

The Implementation I did feels very jank.

Tests:
  3785 passing (41.8s)
  42 failing
@KarimElsayad247
Copy link
Author

With the addition of from_interpolation failing tests are down to 42. Another 124 tests now pass.

image

That said, the way I added the metadata field feels very hacky. It might be premature to think about it, but I introduced an Array -> List -> Array conversion step. It's not necessary if that particular metadata field is guaranteed to only have from_interpolation and the line number.

I think that's it for the easy gains, the remaining 42 look scary.

# Conflicts:
#	build.gradle.kts
@KarimElsayad247
Copy link
Author

KarimElsayad247 commented Feb 3, 2026

Getting slightly irked

Here, not does not make a block

  def drop(keywords, keys) when is_list(keywords) and is_list(keys) do
    :lists.filter(fn {k, _} -> k not in keys end, keywords)
  end

while here, it does

    {options, maybe_integer} =
      options
      |> String.to_charlist()
      |> Enum.split_while(&(&1 not in ?0..?9))

here, NOT sometimes should build a block, and sometimes not

image

I don't see the semantic difference, sometimes it should be a block function call, and sometimes not

I made a change in org/elixir_lang/psi/impl/QuotableImpl.kt:1847 where I removed NOT and EXCLAMATION_POINT

when ((quotedChild as OtpErlangTuple).elementAt(0)) {
    EXCLAMATION_POINT, NOT, UNQUOTE_SPLICING ->
        QuotableImpl.blockFunctionCall(quotedChildren, metadata)
    else ->
        quotedChild
}

Regardless, removing NOT and Exclamation mark from the list results in more tests passing so it's slightly more correct than wrong

3891 passing (49.7s)
  27 failing

image

Do I understand what caused this change? No.

Is it perfect? No, some other instances of "NOT" broke because they
want to be snuggled in __block__

e.g.

This one wants __block__

cache/elixir-1.19.5/lib/elixir/lib/option_parser.ex:778

Enum.split_while(&(&1 not in ?0..?9))

While this one does not

cache/elixir-1.19.5/lib/elixir/lib/keyword.ex:1308

:lists.filter(fn {k, _} -> k not in keys end, keywords)
These tests would fail because the files they parse for testing no
longer exist. I removed those and added other tests for new files
that weren't tested before, resulting in a net increase of 3 tests.
@KarimElsayad247
Copy link
Author

KarimElsayad247 commented Feb 3, 2026

Some tests are no longer applicable since they test non-existing files. When run, they produce file not found errors:

intellij-elixir/cache/elixir-1.19.5/lib/mix/lib/mix/public_key.ex (No such file or directory)java.lang.RuntimeException: java.io.FileNotFoundException: intellij-elixir/cache/elixir-1.19.5/lib/mix/lib/mix/public_key.ex (No such file or directory)

I removed those tests, and other tests in their place for some files that were added, and apparently those tests with the error didn't register before, so now we have 15 actual extra tests.

3900 passing (48s)
25 failing

image

@KarimElsayad247
Copy link
Author

KarimElsayad247 commented Feb 3, 2026

hmmmmm, for unqualifiedNoArgumentCalls

        // if a variable has a `do` block is no longer a variable because the do block acts as keyword arguments.
        if (doBlock != null) {
            val quotedBlockArguments = doBlock.quoteArguments()

            quoted = quotedFunctionCall(
                    identifierText,
                    callMetadata,
                    *quotedBlockArguments
            )
        } else {
            /* @note quotedFunctionCall cannot be used here because in the 3-tuple for function calls, the elements are
              {name, metadata, arguments}, while for an ambiguous call or variable, the elements are
              {name, metadata, context}.  Importantly, context is nil when there is no context while arguments are []
              when there are no arguments. */
            quoted = quotedVariable(
                    identifierText,
                    callMetadata
            )
        }

Especially this part

Importantly, context is nil when there is no context while arguments are [] when there are no arguments.
apparently here @SPEC digits(integer, pos_integer) :: [integer, ...]

The ellipsis used to be a variable, but now they should be a function call?

image

Tests
  3906 passing (52.3s)
  19 failing
@KarimElsayad247
Copy link
Author

Handling ellipsis reduces failing tests to 19

3906 passing (52.3s)
19 failing

When capture expression:
    - `not` is wrapped in __block__

When stabby lambda expression:
    - `not` is NOT wrapped in block

see
KronicDeth#3776 (comment)
I can't figure out what should be done.
@KarimElsayad247
Copy link
Author

I got completely stuck trying to figure out the behavior of NOT. I added a few tests isolating cases where it should be wrapped in a block, and cases where it should not. It feels clear cut: in you find not inside parens, wrap it in a block, but I'm totally unable to express this.

I tried to play around with buildBlock, this is it now:

internal fun buildBlock(quotedChildren: List<OtpErlangObject>, metadata: OtpErlangList): OtpErlangObject =

    /**
     * Builds a block for stab bodies.  Unlike `toBlock`, handles rearranging unary operations `not` and `!` and putting
     * solitary `unquote_splicing` calls in blocks.
     *
     * @param quotedChildren
     */
    @Contract(pure = true)
    internal fun buildBlock(quotedChildren: List<OtpErlangObject>, metadata: OtpErlangList): OtpErlangObject =
            when (quotedChildren.size) {
                0 -> NIL
                1 -> {
                    val quotedChild = quotedChildren.first()

                    // @see https://github.com/elixir-lang/elixir/blob/de39bbaca277002797e52ffbde617ace06233a2b/lib/elixir/src/elixir_parser.yrl#L588
                    if (Macro.isLocalCall(quotedChild)) {
                        // @see https://github.com/elixir-lang/elixir/blob/de39bbaca277002797e52ffbde617ace06233a2b/lib/elixir/src/elixir_parser.yrl#L547
                        when ((quotedChild as OtpErlangTuple).elementAt(0)) {
                            UNQUOTE_SPLICING ->
                                QuotableImpl.blockFunctionCall(quotedChildren, metadata)
                            else ->
                                quotedChild
                        }
                    } else {
                        quotedChild
                    }
                }
                else -> QuotableImpl.blockFunctionCall(quotedChildren, metadata)
            }

I tortured the when clause to look like this in desperation

                        when ((quotedChild as OtpErlangTuple).elementAt(0)) {
                            UNQUOTE_SPLICING ->
                                QuotableImpl.blockFunctionCall(quotedChildren, metadata)
                            EXCLAMATION_POINT, NOT -> {
                                if (node!!.elementType == ElixirTypes.STAB_BODY && node.treeParent.elementType !== ElixirTypes.STAB_OPERATION) {
                                    QuotableImpl.blockFunctionCall(quotedChildren, otpErlangList())
                                } else {
                                    quotedChild
                                }
                            }
                            else ->
                                quotedChild
                        }

to no avail.

The only pattern I could glean is that everything that is supposed to be wrapped with a block is a STAB_BODY, but many other things are STAB_BODY and should not be wrapped in a block.

image

It feels like I need to fully understand the grammar to figure out how to proceed

but look at this thing

https://github.com/KronicDeth/intellij-elixir/blob/main/src/org/elixir_lang/Elixir.bnf

It scares me. It reflects off my smooth brain.

@KarimElsayad247
Copy link
Author

Now I realize why I'm not understanding elixir.bnf

I should've been reading this file instead https://github.com/elixir-lang/elixir/blob/main/lib/elixir/src/elixir_parser.yrl

This `def operator?(:..//, 3)` would result in the following error:

<unmatched expression> expected, got ','

I updated the lexer file to include this operator in the same way
other three token operators are included, then regenerated the lexer.

Regenerating the lexer resulting in some errors addressed below:

Add stack.clear() to reset

because Intellij showed a warning to add it manually to
ElixirFlexLexer.reset because the generator can't do that
automatically when regenerating.

Wrap `myFixture.configureByFile(path)` in a command

An error was being thrown to put undoable actions inside an executeCommand.
The error advised checking

https://github.com/JetBrains/intellij-community/blob/master/platform/core-api/src/com/intellij/openapi/command/CommandProcessor.java
Otherwise, releaseQuoter this step would fail with an error.
This is definitely bad and doesn't address the root issue, but it's
a starting point. At least it would serve as a conversation starter
in a review.
@KarimElsayad247
Copy link
Author

With those recent commits, tests are at

3915 passing (51.1s)
14 failing

image

I also realized something: there are close to a hundred extra tests lmaoo

I didn't look at the first two digits when counting :D

I started with 3827 and now I'm at 3929 :D

This block of code threw a runtime exception while testing for some reason.

The test passes, but it still throws an exception.

From: `/macro.ex:0-10`

### Element Class Name

```
org.elixir_lang.psi.impl.ElixirUnmatchedUnqualifiedNoParenthesesCallImpl
```

com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments:
java.lang.Throwable: Cannot construct CallDefinitionClause from quote's enclosing macro call
@KarimElsayad247
Copy link
Author

It's funky because this error happens in a doc string :D

@KarimElsayad247
Copy link
Author

By handling ambiguous calls, failed test drop to 10

3920 passing (50.9s)
10 failing

image

This form seems to work alright. The txt file became different, however,
so I skipped this test's checkResult step.
Turns out I can just look at the previous char and see it's an open brace.

Tests:
  3922 passing (50.9s)
  8 failing
@joshuataylor
Copy link
Collaborator

Thanks for the amazing work, and for the detailed updates! I'm having a look at this now we've released v23.0.0!

@KarimElsayad247
Copy link
Author

I noticed while testing changes that autocomplete breaks for some reason when you add a call

image image

This issue seems relevant #2980

@joshuataylor
Copy link
Collaborator

Does the actual file have something that can't be parsed? I found this with #3770 (See the screenshots, the file has errors)

@KarimElsayad247
Copy link
Author

KarimElsayad247 commented Feb 11, 2026

@joshuataylor

No, it's a pretty clean file, no errors or anything. Here's a video example

Screencast_20260211_094129.webm

@montogeek
Copy link

@KarimElsayad247 Thanks for all this work :)

joshuataylor and others added 18 commits February 19, 2026 08:39
It was introduced with Elixir 1.14

Tests:
  3574 passing (1m 17s)
  253 failing
It was introduced with Elixir 1.16

The Implementation I did feels very jank.

Tests:
  3785 passing (41.8s)
  42 failing
Do I understand what caused this change? No.

Is it perfect? No, some other instances of "NOT" broke because they
want to be snuggled in __block__

e.g.

This one wants __block__

cache/elixir-1.19.5/lib/elixir/lib/option_parser.ex:778

Enum.split_while(&(&1 not in ?0..?9))

While this one does not

cache/elixir-1.19.5/lib/elixir/lib/keyword.ex:1308

:lists.filter(fn {k, _} -> k not in keys end, keywords)
These tests would fail because the files they parse for testing no
longer exist. I removed those and added other tests for new files
that weren't tested before, resulting in a net increase of 3 tests.
Tests
  3906 passing (52.3s)
  19 failing
When capture expression:
    - `not` is wrapped in __block__

When stabby lambda expression:
    - `not` is NOT wrapped in block

see
KronicDeth#3776 (comment)
I can't figure out what should be done.
This `def operator?(:..//, 3)` would result in the following error:

<unmatched expression> expected, got ','

I updated the lexer file to include this operator in the same way
other three token operators are included, then regenerated the lexer.

Regenerating the lexer resulting in some errors addressed below:

Add stack.clear() to reset

because Intellij showed a warning to add it manually to
ElixirFlexLexer.reset because the generator can't do that
automatically when regenerating.

Wrap `myFixture.configureByFile(path)` in a command

An error was being thrown to put undoable actions inside an executeCommand.
The error advised checking

https://github.com/JetBrains/intellij-community/blob/master/platform/core-api/src/com/intellij/openapi/command/CommandProcessor.java
Otherwise, releaseQuoter this step would fail with an error.
This is definitely bad and doesn't address the root issue, but it's
a starting point. At least it would serve as a conversation starter
in a review.
This block of code threw a runtime exception while testing for some reason.

The test passes, but it still throws an exception.

From: `/macro.ex:0-10`

### Element Class Name

```
org.elixir_lang.psi.impl.ElixirUnmatchedUnqualifiedNoParenthesesCallImpl
```

com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments:
java.lang.Throwable: Cannot construct CallDefinitionClause from quote's enclosing macro call
This form seems to work alright. The txt file became different, however,
so I skipped this test's checkResult step.
Turns out I can just look at the previous char and see it's an open brace.

Tests:
  3922 passing (50.9s)
  8 failing
@KarimElsayad247
Copy link
Author

KarimElsayad247 commented Mar 2, 2026

Test results after merging the recent fix for JSP currently in main branch:

3969 passing (50.1s)
6 failing

image

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.

3 participants