Skip to content

Commit 173014e

Browse files
committed
Add back custom functions
1 parent 0fc0777 commit 173014e

34 files changed

+525
-456
lines changed

docs/dev-env-stacks.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# How LibSass handles variables, functions and mixins
2+
3+
This document is intended for developers of LibSass only and are of now use
4+
for implementers. It documents how variable stacks are implemented.
5+
6+
## Foreword
7+
8+
LibSass uses an optimized stack approach similar to how C compilers have always
9+
done it, by using a growable stack where we can push and pop items of. Unfortunately
10+
Sass has proven to be a bit more dynamic than static optimizers like, therefore we
11+
had to adopt the principle a little to accommodate the edge-cases due to this.
12+
13+
There are three different kind of entities on the stack during runtime, namely
14+
variables, functions and mixins. Each has it's own dedicated stack to optimize
15+
the lookups. In this doc we will often only cover one case, but it should be
16+
applicable to any other stack object (with some small differences).
17+
18+
Also for regular sass code and style-rules we wouldn't need this setup, but
19+
it becomes essential to correctly support mixins and functions, since those
20+
can be called recursively. It is also vital for loops, like @for or @each.
21+
22+
## Overview
23+
24+
The whole process is split into two main phases. In order to correctly support
25+
@import we had to introduce the preloader phase, where all @use, @forward and
26+
@import rules are loaded first, before any evaluation happens. This ensures that
27+
we know all entities before the evaluation phase in order to correctly setup
28+
all stack frames.
29+
30+
## Basic example
31+
32+
Let's assume we have the following scss code:
33+
34+
```scss
35+
$a: 1;
36+
b {
37+
$a: 2;
38+
}
39+
```
40+
41+
This will allocate two independent variables on the stack. For easier reference
42+
we can think of them as variable 0 and variable 1. So let's see what happens if
43+
we introduce some VariableExpressions:
44+
45+
```scss
46+
$a: 1;
47+
b {
48+
a0: $a;
49+
$a: 2;
50+
a1: $a;
51+
}
52+
c {
53+
a: $a;
54+
}
55+
```
56+
57+
As you may have guesses, the `a0` expression will reference variable 0 and the
58+
`a1` expression will reference variable 1, while the last one will reference
59+
variable 0 again. Given this easy example this might seem overengineered, but
60+
let's see what happens if we introduce a loop:
61+
62+
```
63+
$a: 1;
64+
b {
65+
@for $x from 1 through 2 {
66+
a0: $a;
67+
$a: 2;
68+
a1: $a;
69+
}
70+
}
71+
c {
72+
a: $a;
73+
}
74+
```
75+
76+
Here I want to concentrate on `a0`. In most programing languages, `a0: $a` would
77+
always point to variable 0, but in Sass this is more dynamic. It will actually
78+
reference variable 0 on the first run, and variable 1 on consecutive runs.
79+
80+
## What is an EnvFrame and EnvRef
81+
82+
Whenever we encounter a new scope while parsing, we will create a new EnvFrame.
83+
Every EnvFrame (often also just called idxs) knows the variables, functions and
84+
mixins that are declared within that scope. Each entity is simply referenced by
85+
it's integer offset (first variable, second variable and so on). Each frame is
86+
stored as long as the context/compiler lives. In order to find functions, each
87+
frame keeps a hash-map to get the local offset for an entity name (e.g. varIdxs).
88+
An EnvRef is just a struct with the env-frame address and the local entity offset.
89+
90+
## Where are entities actually stored during runtime
91+
92+
The `EnvRoot` has a growable stack for each entity type. Whenever we evaluate
93+
a lexical scope, we will push the entities to the stack to bring them live.
94+
By doing this, we also update the current pointer for the given env-frame to
95+
point to the correct position within that stack. Let's see how this works:
96+
97+
```scss
98+
$a: 1;
99+
@function recursive($abort) {
100+
$a: $a + 1;
101+
@if ($abort) {
102+
@return $a;
103+
}
104+
@else {
105+
@return recursive(true);
106+
}
107+
}
108+
a {
109+
b: recursive(false);
110+
}
111+
```
112+
113+
Here we call the recursive function twice, so the `$a` inside must be independent.
114+
The stack allocation would look the following in this case:
115+
116+
- Entering root scope
117+
- pushing one variable on the runtime var stack.
118+
- Entering for scope for the first time
119+
- updating varFramePtr to 1 since there is already one variable.
120+
- pushing another variable on the runtime var stack
121+
- Entering for scope for the second time
122+
- updating varFramePtr to 2 since there are now two variable.
123+
- pushing another variable on the runtime var stack
124+
- Exiting second for scope and restoring old state
125+
- Exiting first for scope and restoring old state
126+
- Exiting root scope
127+
128+
So in the second for loop run, when we have to resolve the variable expression for `$a`,
129+
we first get the base frame pointer (often called stack frame pointer in C). Then we only
130+
need to add the local offset to get to the current frame instance of the variable. Once we
131+
exit a scope, we simply need to pop those entities off the stack and reset the frame pointer.
132+
133+
## How ambiguous/dynamic lookup is done in loops
134+
135+
Unfortunately we are not able to fully statically optimize the variable lookups (as explained
136+
earlier, due to the full dynamic nature of Sass). IMO we can do it for functions and mixins,
137+
as they always have to be declared and defined at the same time. But variables can also just
138+
be declared and defined later. So in order to optimize this situation we will first fetch and
139+
cache all possible variable declarations (vector of VarRef). Then on evaluation we simply need
140+
to check and return the first entity that was actually defined (assigned to).
141+
142+
## Afterword
143+
144+
I hope this example somehow clarifies how variable stacks are implemented in LibSass. This
145+
optimization can easily bring 50% or more performance in contrast to always do the dynamic
146+
lookup via the also available hash-maps. They are needed anyway for meta functions, like
147+
`function-exists`. It is also not possible to (easily) create new variables once the parsing
148+
is done, so the C-API doesn't allow to create new variables during runtime.

src/ast_expressions.hpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,8 @@ namespace Sass {
282282
class VariableExpression final : public Expression
283283
{
284284
ADD_CONSTREF(EnvKey, name);
285-
ADD_REF(sass::vector<EnvIdx>, vidxs);
286-
// ADD_REF(EnvIdx, vidx2);
285+
ADD_REF(sass::vector<EnvRef>, vidxs);
286+
// ADD_REF(EnvRef, vidx2);
287287
ADD_CONSTREF(sass::string, ns);
288288
ADD_PROPERTY(bool, withinLoop);
289289
public:
@@ -390,7 +390,7 @@ namespace Sass {
390390
ADD_CONSTREF(sass::string, name);
391391

392392
// The frame offset for the function
393-
ADD_REF(EnvIdx, fidx2);
393+
ADD_REF(EnvRef, fidx2);
394394

395395
// Internal optimization flag
396396
ADD_CONSTREF(bool, withinLoop);

src/ast_fwd_decl.hpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@
1919
/////////////////////////////////////////////
2020
namespace Sass {
2121

22-
class Env;
23-
class EnvRoot;
22+
// Forward declare
2423
class EnvKey;
24+
class EnvRef;
25+
class EnvRefs;
26+
class EnvRoot;
27+
class EnvFrame;
28+
2529
class Module;
2630
class BuiltInMod;
2731
class WithConfig;
2832

29-
class EnvIdx;
30-
class EnvRefs;
3133
class Logger;
3234
class Compiler;
3335
class EnvFrame;

src/ast_nodes.cpp

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ namespace Sass {
296296
// The SassScript `>` operation.
297297
bool Value::greaterThan(Value* other, Logger& logger, const SourceSpan& pstate) const
298298
{
299-
logger.addFinalStackTrace(pstate);
299+
callStackFrame csf(logger, pstate);
300300
throw Exception::SassScriptException(
301301
"Undefined operation \"" + inspect()
302302
+ " > " + other->inspect() + "\".",
@@ -307,7 +307,7 @@ namespace Sass {
307307
// The SassScript `>=` operation.
308308
bool Value::greaterThanOrEquals(Value* other, Logger& logger, const SourceSpan& pstate) const
309309
{
310-
logger.addFinalStackTrace(pstate);
310+
callStackFrame csf(logger, pstate);
311311
throw Exception::SassScriptException(
312312
"Undefined operation \"" + inspect()
313313
+ " >= " + other->inspect() + "\".",
@@ -318,7 +318,7 @@ namespace Sass {
318318
// The SassScript `<` operation.
319319
bool Value::lessThan(Value* other, Logger& logger, const SourceSpan& pstate) const
320320
{
321-
logger.addFinalStackTrace(pstate);
321+
callStackFrame csf(logger, pstate);
322322
throw Exception::SassScriptException(
323323
"Undefined operation \"" + inspect()
324324
+ " < " + other->inspect() + "\".",
@@ -329,7 +329,7 @@ namespace Sass {
329329
// The SassScript `<=` operation.
330330
bool Value::lessThanOrEquals(Value* other, Logger& logger, const SourceSpan& pstate) const
331331
{
332-
logger.addFinalStackTrace(pstate);
332+
callStackFrame csf(logger, pstate);
333333
throw Exception::SassScriptException(
334334
"Undefined operation \"" + inspect()
335335
+ " <= " + other->inspect() + "\".",
@@ -340,7 +340,7 @@ namespace Sass {
340340
// The SassScript `*` operation.
341341
Value* Value::times(Value* other, Logger& logger, const SourceSpan& pstate) const
342342
{
343-
logger.addFinalStackTrace(pstate);
343+
callStackFrame csf(logger, pstate);
344344
throw Exception::SassScriptException(
345345
"Undefined operation \"" + inspect()
346346
+ " * " + other->inspect() + "\".",
@@ -351,7 +351,7 @@ namespace Sass {
351351
// The SassScript `%` operation.
352352
Value* Value::modulo(Value* other, Logger& logger, const SourceSpan& pstate) const
353353
{
354-
logger.addFinalStackTrace(pstate);
354+
callStackFrame csf(logger, pstate);
355355
throw Exception::SassScriptException(
356356
"Undefined operation \"" + inspect()
357357
+ " % " + other->inspect() + "\".",
@@ -441,7 +441,7 @@ namespace Sass {
441441
// Assert and return a color or throws if incompatible
442442
const Color* Value::assertColor(Logger& logger, const sass::string& name) const
443443
{
444-
logger.addFinalStackTrace(pstate());
444+
callStackFrame csf(logger, pstate());
445445
throw Exception::SassScriptException(
446446
inspect() + " is not a color.",
447447
logger, pstate(), name);
@@ -450,7 +450,7 @@ namespace Sass {
450450
// Assert and return a function or throws if incompatible
451451
Function* Value::assertFunction(Logger& logger, const sass::string& name)
452452
{
453-
logger.addFinalStackTrace(pstate());
453+
callStackFrame csf(logger, pstate());
454454
throw Exception::SassScriptException(
455455
inspect() + " is not a function reference.",
456456
logger, pstate(), name);
@@ -459,7 +459,7 @@ namespace Sass {
459459
// Assert and return a map or throws if incompatible
460460
Map* Value::assertMap(Logger& logger, const sass::string& name)
461461
{
462-
logger.addFinalStackTrace(pstate());
462+
callStackFrame csf(logger, pstate());
463463
throw Exception::SassScriptException(
464464
inspect() + " is not a map.",
465465
logger, pstate(), name);
@@ -468,7 +468,7 @@ namespace Sass {
468468
// Assert and return a number or throws if incompatible
469469
Number* Value::assertNumber(Logger& logger, const sass::string& name)
470470
{
471-
logger.addFinalStackTrace(pstate());
471+
callStackFrame csf(logger, pstate());
472472
throw Exception::SassScriptException(
473473
inspect() + " is not a number.",
474474
logger, pstate(), name);
@@ -484,7 +484,7 @@ namespace Sass {
484484
// Assert and return a string or throws if incompatible
485485
String* Value::assertString(Logger& logger, const sass::string& name)
486486
{
487-
logger.addFinalStackTrace(pstate());
487+
callStackFrame csf(logger, pstate());
488488
throw Exception::SassScriptException(
489489
inspect() + " is not a string.",
490490
logger, pstate(), name);
@@ -508,7 +508,7 @@ namespace Sass {
508508
// Assert and return an argument list or throws if incompatible
509509
ArgumentList* Value::assertArgumentList(Logger& logger, const sass::string& name)
510510
{
511-
logger.addFinalStackTrace(pstate());
511+
callStackFrame csf(logger, pstate());
512512
throw Exception::SassScriptException(
513513
inspect() + " is not an argument list.",
514514
logger, pstate(), name);

src/ast_statements.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ namespace Sass {
5555
for (auto cfgvar : config) {
5656
if (cfgvar.second.wasUsed == false) {
5757
if (cfgvar.second.isGuarded == false) {
58-
logger.addFinalStackTrace(cfgvar.second.pstate2);
58+
callStackFrame csf(logger, cfgvar.second.pstate2);
5959
throw Exception::RuntimeException(logger, "$" +
6060
cfgvar.second.name + " was not declared "
6161
"with !default in the @used module.");
@@ -633,7 +633,7 @@ namespace Sass {
633633
const EnvKey& variable,
634634
bool withinLoop,
635635
const sass::string ns,
636-
sass::vector<EnvIdx> vidxs,
636+
sass::vector<EnvRef> vidxs,
637637
Expression* value,
638638
bool is_default,
639639
bool is_global) :

src/ast_statements.hpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ namespace Sass {
391391
public CallableDeclaration
392392
{
393393
// Content function reference
394-
ADD_CONSTREF(EnvIdx, cidx);
394+
ADD_CONSTREF(EnvRef, cidx);
395395

396396
ADD_CONSTREF(UserDefinedCallableObj, cmixin);
397397

@@ -419,7 +419,7 @@ namespace Sass {
419419
public CallableDeclaration
420420
{
421421
// Function reference
422-
ADD_CONSTREF(EnvIdx, fidx);
422+
ADD_CONSTREF(EnvRef, fidx);
423423
public:
424424
// Value constructor
425425
FunctionRule(
@@ -445,9 +445,9 @@ namespace Sass {
445445
public CallableDeclaration
446446
{
447447
// Mixin function reference
448-
ADD_CONSTREF(EnvIdx, midx);
448+
ADD_CONSTREF(EnvRef, midx);
449449
// Content function reference
450-
// ADD_CONSTREF(EnvIdx, cidx33);
450+
// ADD_CONSTREF(EnvRef, cidx33);
451451
public:
452452
// Value constructor
453453
MixinRule(
@@ -750,9 +750,9 @@ namespace Sass {
750750
ADD_CONSTREF(EnvKey, variable);
751751
ADD_CONSTREF(sass::string, ns);
752752
ADD_CONSTREF(ExpressionObj, value);
753-
ADD_REF(sass::vector<EnvIdx>, vidxs);
753+
ADD_REF(sass::vector<EnvRef>, vidxs);
754754

755-
ADD_REF(EnvIdx, vidx2);
755+
ADD_REF(EnvRef, vidx2);
756756
ADD_PROPERTY(bool, withinLoop);
757757

758758
ADD_CONSTREF(bool, is_default); // ToDO rename
@@ -764,7 +764,7 @@ namespace Sass {
764764
const EnvKey& variable,
765765
bool withinLoop,
766766
const sass::string ns,
767-
sass::vector<EnvIdx> vidxs,
767+
sass::vector<EnvRef> vidxs,
768768
Expression* value,
769769
bool is_default = false,
770770
bool is_global = false);
@@ -792,7 +792,7 @@ namespace Sass {
792792
// The name of the mixin being invoked.
793793
ADD_CONSTREF(EnvKey, name);
794794

795-
ADD_CONSTREF(EnvIdx, midx);
795+
ADD_CONSTREF(EnvRef, midx);
796796

797797
// The block that will be invoked for [ContentRule]s in the mixin
798798
// being invoked, or `null` if this doesn't pass a content block.

0 commit comments

Comments
 (0)