less / less/less.js

Multiple nested lambda-style are exponentially slow

Open
#2,570 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
JavaScript
Stars
17k
Forks
3.4k
Avg merge
7h 42m
Merged PRs (30d)
26

Description

This is a followup of #2556.

I ended up needing to pass the @content fake-lambda as a parameter as well.

Now, my LESS hangs when compiled.

I stripped it down to the bare minimum (shown below), and it takes over a minute to compile.

I tried debugging the compiler, and found that env.frames grows to contain thousands of entries, but I'm not sure how to optimize.

Stripped-down source:

.fullAdder(@c, @prefix, @carryIn) {
    @prefixFormat: %(~'#%s-%%s', @prefix);
    .op(@c,
        { .c(@c) { .op(@c, %(@prefixFormat, a), and, %(@prefixFormat, b)) } },
        or,
        { .c(@c) { .op(@c,
            @carryIn,
            and,
            { .c(@c) { .op(@c, %(@prefixFormat, a), xor, %(@prefixFormat, b)); } }
        ); } }
    );
}



.op(
    abc, and, 
    { .c(@c) { .fullAdder(@c, 
        chained-adder-4,
        { .c(@c) { .fullAdder(@c,
            chained-adder-3,
            { .c(@c) { .fullAdder(@c,
                chained-adder-2,
                { .c(@c) { 
                    .op(@c, chained-adder-1-a, and, chained-adder-1-b); 
                } }
            ); } }
        ); } }
    ); } }, {
        color: red;
    }
);






// The .op() mixin calls its @content callback iff the
// specified logical operation evaluates to true.  The
// input parameters (@first and @second) can either be
// selectors to combine (typically :checked selectors)
// or detached rulesets containing further .op() calls
// to chain.
// To pass a .op() call, use the following syntax:
// { .c(@c) { .op(@c, blah, xor, blah); } }
// We need this complexity to pass @invertCombinator &
// @content from the outer call; see 
// https://github.com/less/less.js/issues/2558

// This overload is called by the outermost operation,
// as opposed to operations passed as detached ruleset 
// parameters.  It calls the actual overloads with the
// default !invertCombinator and the explicitly-passed
// @content parameter
.op(@first, @operation, @second, @content) when (@operation = and), (@operation = xor), (@operation = or)  {
    .op(false @content, @first, @operation, @second);   // Pass @content via scope
}

// These overloads are called in detached rulesets when chaining
.op(@c, @first, @operation, @second, @invertCombinator: extract(@c, 1), @content: extract(@c, 2)) when (@operation = xor) and not (@invertCombinator) {
    #private.callInverted(~'', @first, {
        #private.call(~' ~ ', @second, @content);
    });
    #private.call(~'', @first, {
        #private.callInverted(~' ~ ', @second, @content);
    });
}
.op(@c, @first, @operation, @second, @invertCombinator: extract(@c, 1), @content: extract(@c, 2)) when (@operation = xor) and (@invertCombinator) {
    #private.callInverted(~'', @first, {
        #private.callInverted(~' ~ ', @second, @content);
    });
    #private.call(~'', @first, {
        #private.call(~' ~ ', @second, @content);
    });
}

.op(@c, @first, @operation, @second, @invertCombinator: extract(@c, 1), @content: extract(@c, 2)) when (@operation = or) and not (@invertCombinator) {
    #private.call(~'', @first, @content);
    #private.call(~'', @second, @content);
}
.op(@c, @first, @operation, @second, @invertCombinator: extract(@c, 1), @content: extract(@c, 2)) when (@operation = or) and (@invertCombinator) {
    #private.callInverted(~'', @first, {
        #private.callInverted(~' ~ ', @second, @content);
    });
}

.op(@c, @first, @operation, @second, @invertCombinator: extract(@c, 1), @content: extract(@c, 2)) when (@operation = and) and not (@invertCombinator) {
    #private.call(~'', @first, {
        #private.call(~' ~ ', @second, @content);
    });
}
.op(@c, @first, @operation, @second, @invertCombinator: extract(@c, 1), @content: extract(@c, 2)) when (@operation = and) and (@invertCombinator) {
    #private.callInverted(~'', @first, @content);
    #private.callInverted(~'', @second, @content);
}

#private {
    // Calls a selector (as a string or a nested ruleset),
    // inside the context that this selector was called in
    .call(@combinator, @selector, @content) when (isString(@selector)) {
        &@{combinator}@{selector}:checked { @content(); }
    }
    .call(@combinator, @selector, @content) when  (isKeyword(@selector)) {
        &@{combinator}#@{selector}:checked { @content(); }
    }

    .call(@combinator, @selector, @content) when (default()) {  // isruleset(@selector) doesn't work
        &@{combinator} {
            @selector();
            .c(false @content);
        }
    }

    // Same as above, but passes @invertCombinator
    // to invert the result of the combinator.
    .callInverted(@combinator, @selector, @content) when (isString(@selector)) {
        &@{combinator}@{selector}:not(:checked) { @content(); }
    }
    .callInverted(@combinator, @selector, @content) when (isKeyword(@selector)) {
        &@{combinator}#@{selector}:not(:checked) { @content(); }
    }

    .callInverted(@combinator, @selector, @content) when (default()) {  // isruleset(@selector) doesn't work
        &@{combinator} {
            @selector();
            .c(true @content);
        }
    }
}

Removing the six copies of , @content: extract(@c, 2) (which flow the @content through the parameter instead of through scope) makes the compilation time sane, but breaks the output. You can see the current, broken output behavior here (source); it skips the second half of a doubly-nested combinator.

You can see the minimal case that necessitates this fix here.

Any idea how to make this work?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the minimal LESS case in the issue and the linked gist, then inspect how the compiler's env.frames grows while nested detached rulesets pass @content through extract(@c, 2). Compare compilation time and generated output against the Silon example; done means the nested case compiles without exponential slowdown while preserving the missing second combinator half.

Written by the indexing model from the issue text.

Assessment

Tech stack
css, javascript
Domain
compilers, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.