Skip to content

rgb(from … r g b / <alpha>) throws under --math=always (Operation on an invalid type) #4528

Open

Description

@zslabs

To reproduce:

CSS Color Level 5 relative-color syntax with an alpha slash. This is valid CSS that should be left for the browser.

Playground (4.9.1, --math=always):
https://lesscss.org/less-preview/#eyJjb2RlIjoiLmNhcmQge1xuICAvKiBDU1MgQ29sb3IgTGV2ZWwgNSByZWxhdGl2ZSBjb2xvciArIGFscGhhICovXG4gIGJhY2tncm91bmQ6IHJnYihmcm9tIHZhcigtLXN1cmZhY2UpIHIgZyBiIC8gMC45KTtcbn1cbiIsImFjdGl2ZVZlcnNpb24iOiI0LjkuMSIsIm1hdGgiOiJhbHdheXMiLCJzdHJpY3RVbml0cyI6ZmFsc2V9

Or locally:

.card {
  background: rgb(from var(--surface) r g b / 0.9);
}
npx lessc --math=always input.less

The origin does not have to be var(). A literal hex fails the same way:

.card {
  background: rgb(from #112233 r g b / 0.9);
}

Current behavior:

Less 4.4.0+ with --math=always throws:

Error evaluating function `rgb`: Operation on an invalid type

Less treats the CSS alpha slash as division. After #4346, b is a colorOperand, so b / 0.9 becomes a Less Operation and eval blows up.

Expected behavior:

Emit the declaration unchanged, same as 4.3.0 and same as 4.9.1 with --math=parens-division:

.card {
  background: rgb(from var(--surface) r g b / 0.9);
}

That matches the pass-through already expected by the tests added in #4346 / #4434 for rgb(from #0000FF calc(r + 1) g b).

Environment information:

  • less version: 4.9.1 (also reproduced on 4.4.0+. 4.3.0 is fine)
  • nodejs version: 24.12.0
  • operating system: macOS

Additional context:

rgb() as a Less color constructor is not new. The regression is that relative-color CSS started parsing as a Less rgb() call, then the / <alpha> slash is evaluated as math.

Input 4.3.0 --math=always 4.9.1 --math=always 4.9.1 --math=parens-division
rgb(from var(--x) r g b / 0.9) pass-through error pass-through
rgb(from #112233 r g b / 0.9) pass-through error pass-through
rgb(from var(--x) r g b) (no slash) pass-through pass-through pass-through
rgb(17, 34, 51) (Less constructor) #112233 #112233 #112233

Related:

  • #4343 / #4346 (v4.4.0): added colorOperand ([lchrgbs]) so oklch(from …) / rgb(from … calc(r + 1) …) would parse instead of Could not parse call arguments or missing ')'.
  • #4316 / #4434 (v4.6.6): tightened the operand regex so b matches before /.
  • #4479 (v4.8.1): same class of bug for math functions (sin(var(--x)) etc.). Those now return nothing and leave the call for the browser. rgb() does not get that treatment.

Workaround that should not be required: quote the value so Less does not parse it (~'rgb(from var(--surface) r g b / 0.9)'), or compile with --math=parens-division.

Activity

  1. dosubot commented on Sep 8, 2026

    @dosubot

    Hi @zslabs! I'm Dosu and I'm helping the Personal team.

    Your root-cause analysis is accurate. The colorOperand grammar rule added in #4346 (/^[lchrgbs]\s+/) causes b to match as an operand, so the parser builds a Less Operation node for b / 0.9 instead of leaving it as CSS text.

    In Operation.eval(), under --math=always, context.isMathOn(this.op) returns true for /, and because b evaluates to a Keyword (which has no .operate method), the code hits the throw { type: 'Operation', message: 'Operation on an invalid type' } path [1]. The existing escape hatch on line 35 only fires when context.math === MATH.PARENS_DIVISION, which explains why --math=parens-division works fine while --math=always blows up.

    This is the same class of issue addressed by #4479 for math functions like sin(var(--x)), where MathHelper was updated to return undefined (leaving the call for the browser) when the argument is an unresolvable Call node. The rgb() color function path doesn't get that treatment — the rgb handler in color.js expects b in b / 0.9 to be destructurable as a division Operation with two numeric operands, which fails for the relative-color case.

    I didn't find an existing PR or issue tracking this specific rgb(from ... / alpha) + --math=always regression, so this looks like a new report. A fix would likely need to either:

    1. Teach Operation.eval() to fall back to pass-through when operands are unresolvable Keyword/Call nodes (regardless of math mode), or
    2. Have the rgb/hsl color functions detect the relative-color from syntax and bail out early, returning nothing so the declaration is left for the browser — mirroring the MathHelper approach from #4479.

    Want me to dig further into which approach fits best with the existing codebase?

    To reply, just mention @dosu.


    Docs are dead. Just use Dosu.

    Leave Feedback Ask Dosu about less.js Share Dosu with your team

  2. zslabs commented on Sep 8, 2026

    @zslabs
    Author

    I'm working on a PR against master.

    rgb() never sees from here. functionCaller evaluates arguments first, and the slash is already a Less Operation on the channel keyword (b / 0.9). Under --math=always that throws Operation on an invalid type before the color function runs.

    Default parens-division already leaves / alone, which is why this only shows up with math: 'always'.

    The rgb/hsl from early-out from the earlier note is not enough on its own. The operation has to pass through when an operand is a Keyword (channel names) or a Call (var()). #4480 only covers Call, so a hex origin like rgb(from #112233 r g b / 0.9) still dies.

  3. zslabs commented on Sep 8, 2026

    @zslabs
    Author

    PR is up: #4529

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions