Skip to content

Commit 9cff4ca

Browse files
committed
Start writing implementation documentation
Also rename Dispatcher to CompilationDispatcher for clarity
1 parent ca2be2a commit 9cff4ca

File tree

22 files changed

+604
-29
lines changed

22 files changed

+604
-29
lines changed

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Want to contribute? Great! First, read this page.
99
* [Synchronizing](#synchronizing)
1010
* [File Headers](#file-headers)
1111
* [Release Process](#release-process)
12+
* [Package Structure](#package-structure)
1213

1314
## Before You Contribute
1415

@@ -233,3 +234,13 @@ few things to do before pushing that tag:
233234

234235
You *don't* need to create tags for packages in `pkg`; that will be handled
235236
automatically by GitHub actions.
237+
238+
## Package Structure
239+
240+
The structure of the Sass package is documented in README.md files in most
241+
directories under `lib/`. This documentation is intended to help contributors
242+
quickly build a basic understanding of the structure of the compiler and how its
243+
various pieces fit together. [`lib/src/README.md`] is a good starting point to get
244+
an overview of the compiler as a whole.
245+
246+
[`lib/src/README.md`]: lib/src/README.md

lib/src/README.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# The Sass Compiler
2+
3+
* [Life of a Compilation](#life-of-a-compilation)
4+
* [Late Parsing](#late-parsing)
5+
* [Early Serialization](#early-serialization)
6+
* [JS Support](#js-support)
7+
* [APIs](#apis)
8+
* [Importers](#importers)
9+
* [Custom Functions](#custom-functions)
10+
* [Loggers](#loggers)
11+
* [Built-In Functions](#built-in-functions)
12+
* [`@extend`](#extend)
13+
14+
This is the root directory of Dart Sass's private implementation libraries. This
15+
contains essentially all the business logic defining how Sass is actually
16+
compiled, as well as the APIs that users use to interact with Sass. There are
17+
two exceptions:
18+
19+
* [`../../bin/sass.dart`] is the entrypoint for the Dart Sass CLI (on all
20+
platforms). While most of the logic it runs exists in this directory, it does
21+
contain some logic to drive the basic compilation logic and handle errors. All
22+
the most complex parts of the CLI, such as option parsing and the `--watch`
23+
command, are handled in the [`executable`] directory. Even Embedded Sass runs
24+
through this entrypoint, although it gets immediately gets handed off to [the
25+
embedded compiler].
26+
27+
[`../../bin/sass.dart`]: ../../bin/sass.dart
28+
[`executable`]: executable
29+
[the embedded compiler]: embedded/README.md
30+
31+
* [`../sass.dart`] is the entrypoint for the public Dart API. This is what's
32+
loaded when a Dart package imports Sass. It just contains the basic
33+
compilation functions, and exports the rest of the public APIs from this
34+
directory.
35+
36+
[`../sass.dart`]: ../sass.dart
37+
38+
Everything else is contained here, and each file and some subdirectories have
39+
their own documentation. But before you dive into those, let's take a look at
40+
the general lifecycle of a Sass compilation.
41+
42+
## Life of a Compilation
43+
44+
Whether it's invoked through the Dart API, the JS API, the CLI, or the embedded
45+
host, the basic process of a Sass compilation is the same. Sass is implemented
46+
as an AST-walking [interpreter] that operates in roughly three passes:
47+
48+
[interpreter]: https://en.wikipedia.org/wiki/Interpreter_(computing)
49+
50+
1. **Parsing**. The first step of a Sass compilation is always to parse the
51+
source file, whether it's SCSS, the indented syntax, or CSS. The parsing
52+
logic lives in the [`parse`] directory, while the abstract syntax tree that
53+
represents the parsed file lives in [`ast/sass`].
54+
55+
[`parse`]: parse/README.md
56+
[`ast/sass`]: ast/sass/README.md
57+
58+
2. **Evaluation**. Once a Sass file is parsed, it's evaluated by
59+
[`visitor/async_evaluate.dart`]. (Why is there both an async and a sync
60+
version of this file? See [Synchronizing] for details!) The evaluator handles
61+
all the Sass-specific logic: it resolves variables, includes mixins, executes
62+
control flow, and so on. As it goes, it builds up a new AST that represents
63+
the plain CSS that is the compilation result, which is defined in
64+
[`ast/css`].
65+
66+
[`visitor/async_evaluate.dart`]: visitor/async_evaluate.dart
67+
[Synchronizing]: ../../CONTRIBUTING.md#synchronizing
68+
[`ast/css`]: ast/css/README.md
69+
70+
Sass evaluation is almost entirely linear: it begins at the first statement
71+
of the file, evaluates it (which may involve evaluating its nested children),
72+
adds its result to the CSS AST, and then moves on to the second statement. On
73+
it goes until it reaches the end of the file, at which point it's done. The
74+
only exception is module resolution: every Sass module has its own compiled
75+
CSS AST, and once the entrypoint file is done compiling the evaluator will go
76+
back through these modules, resolve `@extend`s across them as necessary, and
77+
stitch them together into the final stylesheet.
78+
79+
SassScript, the expression-level syntax, is handled by the same evaluator.
80+
The main difference between SassScript and statement-level evaluation is that
81+
the same SassScript values are used during evaluation _and_ as part of the
82+
CSS AST. This means that it's possible to end up with a Sass-specific value,
83+
such as a map or a first-class function, as the value of a CSS declaration.
84+
If that happens, the Serialization phase will signal an error when it
85+
encounters the invalid value.
86+
87+
3. **Serialization**. Once we have the CSS AST that represents the compiled
88+
stylesheet, we need to convert it into actual CSS text. This is done by
89+
[`visitor/serialize.dart`], which walks the AST and builds up a big buffer of
90+
the resulting CSS. It uses [a special string buffer] that tracks source and
91+
destination locations in order to generate [source maps] as well.
92+
93+
[`visitor/serialize.dart`]: visitor/serialize.dart
94+
[a special string buffer]: util/source_map_buffer.dart
95+
[source maps]: https://web.dev/source-maps/
96+
97+
There's actually one slight complication here: the first and second pass aren't
98+
as separate as they appear. When one Sass stylesheet loads another with `@use`,
99+
`@forward`, or `@import`, that rule is handled by the evaluator and _only at
100+
that point_ is the loaded file parsed. So in practice, compilation actually
101+
switches between parsing and evaluation, although each individual stylesheet
102+
naturally has to be parsed before it can be evaluated.
103+
104+
### Late Parsing
105+
106+
Some syntax within a stylesheet is only parsed _during_ evaluation. This allows
107+
authors to use `#{}` interpolation to inject Sass variables and other dynamic
108+
values into various locations, such as selectors, while still allowing Sass to
109+
parse them to support features like nesting and `@extend`. The following
110+
syntaxes are parsed during evaluation:
111+
112+
* [Selectors](parse/selector.dart)
113+
* [`@keyframes` frames](parse/keyframe_selector.dart)
114+
* [Media queries](parse/media_query.dart) (for historical reasons, these are
115+
parsed before evaluation and then _reparsed_ after they've been fully
116+
evaluated)
117+
118+
### Early Serialization
119+
120+
There are also some cases where the evaluator can serialize values before the
121+
main serialization pass. For example, if you inject a variable into a selector
122+
using `#{}`, that variable's value has to be converted to a string during
123+
evaluation so that the evaluator can then parse and handle the newly-generated
124+
selector. The evaluator does this by invoking the serializer _just_ for that
125+
specific value. As a rule of thumb, this happens anywhere interpolation is used
126+
in the original stylesheet, although there are a few other circumstances as
127+
well.
128+
129+
## JS Support
130+
131+
One of the main benefits of Dart as an implementation language is that it allows
132+
us to distribute Dart Sass both as an extremely efficient stand-alone executable
133+
_and_ an easy-to-install pure-JavaScript package, using the dart2js compilation
134+
tool. However, properly supporting JS isn't seamless. There are two major places
135+
where we need to think about JS support:
136+
137+
1. When interfacing with the filesystem. None of Dart's IO APIs are natively
138+
supported on JS, so for anything that needs to work on both the Dart VM _and_
139+
Node.js we define a shim in the [`io`] directory that will be implemented in
140+
terms of `dart:io` if we're running on the Dart VM or the `fs` or `process`
141+
modules if we're running on Node. (We don't support IO at all on the browser
142+
except to print messages to the console.)
143+
144+
[`io`]: io/README.md
145+
146+
2. When exposing an API. Dart's JS interop is geared towards _consuming_ JS
147+
libraries from Dart, not producing a JS library written in Dart, so we have
148+
to jump through some hoops to make it work. This is all handled in the [`js`]
149+
directory.
150+
151+
[`js`]: js/README.md
152+
153+
## APIs
154+
155+
One of Sass's core features is its APIs, which not only compile stylesheets but
156+
also allow users to provide plugins that can be invoked from within Sass. In
157+
both the JS API, the Dart API, and the embedded compiler, Sass provides three
158+
types of plugins: importers, custom functions, and loggers.
159+
160+
### Importers
161+
162+
Importers control how Sass loads stylesheets through `@use`, `@forward`, and
163+
`@import`. Internally, _all_ stylesheet loads are modeled as importers. When a
164+
user passes a load path to an API or compiles a stylesheet through the CLI, we
165+
just use the built-in [`FilesystemImporter`] which implements the same interface
166+
that we make available to users.
167+
168+
[`FilesystemImporter`]: importer/filesystem.dart
169+
170+
In the Dart API, the importer root class is [`importer/async_importer.dart`].
171+
The JS API and the embedded compiler wrap the Dart importer API in
172+
[`importer/node_to_dart`] and [`embedded/importer`] respectively.
173+
174+
[`importer/async_importer.dart`]: importer/async_importer.dart
175+
[`importer/node_to_dart`]: importer/node_to_dart
176+
[`embedded/importer`]: embedded/importer
177+
178+
### Custom Functions
179+
180+
Custom functions are defined by users of the Sass API but invoked by Sass
181+
stylesheets. To a Sass stylesheet, they look like any other built-in function:
182+
users pass SassScript values to them and get SassScript values back. In fact,
183+
all the core Sass functions are implemented using the Dart custom function API.
184+
185+
Because custom functions take and return SassScript values, that means we need
186+
to make _all_ values available to the various APIs. For Dart, this is
187+
straightforward: we need to have objects to represent those values anyway, so we
188+
just expose those objects publicly (with a few `@internal` annotations here and
189+
there to hide APIs we don't want users relying on). These value types live in
190+
the [`value`] directory.
191+
192+
[`value`]: value/README.md
193+
194+
Exposing values is a bit more complex for other platforms. For the JS API, we do
195+
a bit of metaprogramming in [`node/value`] so that we can return the
196+
same Dart values we use internally while still having them expose a JS API that
197+
feels native to that language. For the embedded host, we convert them to and
198+
from a protocol buffer representation in [`embedded/value.dart`].
199+
200+
[`node/value`]: node/value/README.md
201+
[`embedded/value.dart`]: embedded/value.dart
202+
203+
### Loggers
204+
205+
Loggers are the simplest of the plugins. They're just callbacks that are invoked
206+
any time Dart Sass would emit a warning (from the language or from `@warn`) or a
207+
debug message from `@debug`. They're defined in:
208+
209+
* [`logger.dart`](logger.dart) for Dart
210+
* [`node/logger.dart`](node/logger.dart) for Node
211+
* [`embedded/logger.dart`](embedded/logger.dart) for the embedded compiler
212+
213+
## Built-In Functions
214+
215+
All of Sass's built-in functions are defined in the [`functions`] directory,
216+
including both global functions and functions defined in core modules like
217+
`sass:math`. As mentioned before, these are defined using the standard custom
218+
function API, although in a few cases they use additional private features like
219+
the ability to define multiple overloads of the same function name.
220+
221+
[`functions`]: functions/README.md
222+
223+
## `@extend`
224+
225+
The logic for Sass's `@extend` rule is particularly complex, since it requires
226+
Sass to not only parse selectors but to understand how to combine them and when
227+
they can be safely optimized away. Most of the logic for this is contained
228+
within the [`extend`] directory.
229+
230+
[`extend`]: extend/README.md

lib/src/ast/css/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# CSS Abstract Syntax Tree
2+
3+
This directory contains the abstract syntax tree that represents a plain CSS
4+
file generated by Sass compilation. It differs from other Sass ASTs in two major
5+
ways:
6+
7+
1. Instead of being created by [a parser], it's created by [the evaluator] as it
8+
traverses the [Sass AST].
9+
10+
[a parser]: ../../parse/README.md
11+
[the evaluator]: ../../visitor/async_evaluate.dart
12+
[Sass AST]: ../sass/README.md
13+
14+
2. Because of various Sass features like `@extend` and at-rule hoisting, the CSS
15+
AST is mutable even though all other ASTs are immutable.
16+
17+
**Note:** the CSS AST doesn't have its own representation of declaration values.
18+
Instead, declaration values are represented as [`Value`] objects. This does mean
19+
that a CSS AST can be in a state where some of its values aren't representable
20+
in plain CSS (such as maps)—in this case, [the serializer] will emit an error.
21+
22+
[`Value`]: ../../value/README.md
23+
[the serializer]: ../../visitor/serialize.dart
24+
25+
## Mutable and Immutable Views
26+
27+
Internally, the CSS AST is mutable to allow for operations like hoisting rules
28+
to the root of the AST and updating existing selectors when `@extend` rules are
29+
encountered. However, because mutability poses a high risk for "spooky action at
30+
a distance", we limit access to mutating APIs exclusively to the evaluator.
31+
32+
We do this by having an _unmodifiable_ interface (written in this directory) for
33+
each CSS AST node which only exposes members that don't modify the node in
34+
question. The implementations of those interfaces, which _do_ have modifying
35+
methods, live in the [`modifiable`] directory. We then universally refer to the
36+
immutable node interfaces except specifically in the evaluator, and the type
37+
system automatically ensures we don't accidentally mutate anything we don't
38+
intend to.
39+
40+
[`modifiable`]: modifiable
41+
42+
(Of course, it's always possible to cast an immutable node type to a mutable
43+
one, but that's a very clear code smell that a reviewer can easily identify.)
44+
45+
## CSS Source Files
46+
47+
A lesser-known fact about Sass is that it actually supports _three_ syntaxes for
48+
its source files: SCSS, the indented syntax, and plain CSS. But even when it
49+
parses plain CSS, it uses the Sass AST rather than the CSS AST to represent it
50+
so that parsing logic can easily be shared with the other stylesheet parsers.

lib/src/ast/sass/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Sass Abstract Syntax Tree
2+
3+
This directory contains the abstract syntax tree that represents a Sass source
4+
file, regardless of which syntax it was written in (SCSS, the indented syntax,
5+
or plain CSS). The AST is constructed recursively by [a parser] from the leaf
6+
nodes in towards the root, which allows it to be fully immutable.
7+
8+
[a parser]: ../../parse/README.md
9+
10+
The Sass AST is broken up into three categories:
11+
12+
1. The [statement AST], which represents statement-level constructs like
13+
variable assignments, style rules, and at-rules.
14+
15+
[statement AST]: statement
16+
17+
2. The [expression AST], which represents SassScript expressions like function
18+
calls, operations, and value literals.
19+
20+
[expression AST]: exprssion
21+
22+
3. Miscellaneous AST nodes that are used by both statements and expressions or
23+
don't fit cleanly into either category that live directly in this directory.
24+
25+
The Sass AST nodes are processed (usually from the root [`Stylesheet`]) by [the
26+
evaluator], which runs the logic they encode and builds up a [CSS AST] that
27+
represents the compiled stylesheet. They can also be transformed back into Sass
28+
source using the `toString()` method. Since this is only ever used for debugging
29+
and doesn't need configuration or full-featured indentation tracking, it doesn't
30+
use a full visitor.
31+
32+
[`Stylesheet`]: statement/stylesheet.dart
33+
[the evaluator]: ../../visitor/async_evaluate.dart
34+
[CSS AST]: ../css/README.md

lib/src/ast/selector/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Selector Abstract Syntax Tree
2+
3+
This directory contains the abstract syntax tree that represents a parsed CSS
4+
selector. This AST is constructed recursively by [the selector parser]. It's
5+
fully immutable.
6+
7+
[the selector parser]: ../../parse/selector.dart
8+
9+
Unlike the [Sass AST], which is parsed from a raw source string before being
10+
evaluated, the selector AST is parsed _during evaluation_. This is necessary to
11+
ensure that there's a chance to resolve interpolation before fully parsing the
12+
selectors in question.
13+
14+
[Sass AST]: ../sass/README.md
15+
16+
Although this AST doesn't include any SassScript, it _does_ include a few
17+
Sass-specific constructs: the [parent selector] `&` and [placeholder selectors].
18+
Parent selectors are resolved by [the evaluator] before it hands the AST off to
19+
[the serializer], while placeholders are omitted in the serializer itself.
20+
21+
[parent selector]: parent.dart
22+
[placeholder selectors]: placeholder.dart
23+
[the evaluator]: ../../visitor/async_evaluate.dart
24+
[the serializer]: ../../visitor/serialize.dart

lib/src/embedded/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Embedded Sass Compiler
2+
3+
This directory contains the Dart Sass embedded compiler. This is a special mode
4+
of the Dart Sass command-line executable, only supported on the Dart VM, in
5+
which it uses stdin and stdout to communicate with another endpoint, the
6+
"embedded host", using a protocol buffer-based protocol. See [the embedded
7+
protocol specification] for details.
8+
9+
[the embedded protocol specification]: https://github.com/sass/sass/blob/main/spec/embedded-protocol.md
10+
11+
The embedded compiler has two different levels of dispatchers for handling
12+
incoming messages from the embedded host:
13+
14+
1. The [`IsolateDispatcher`] is the first recipient of each packet. It decodes
15+
the packets _just enough_ to determine which compilation they belong to, and
16+
forwards them to the appropriate compilation dispatcher. It also parses and
17+
handles messages that aren't compilation specific, such as `VersionRequest`.
18+
19+
[`IsolateDispatcher`]: isolate_dispatcher.dart
20+
21+
2. The [`CompilationDispatcher`] fully parses and handles messages for a single
22+
compilation. Each `CompilationDispatcher` runs in a separate isolate so that
23+
the embedded compiler can run multiple compilations in parallel.
24+
25+
[`CompilationDispatcher`]: compilation_dispatcher.dart
26+
27+
Otherwise, most of the code in this directory just wraps Dart APIs to
28+
communicate with their protocol buffer equivalents.

0 commit comments

Comments
 (0)