Skip to content

[RFC] Modernizing the architecture of the code #193

@stof

Description

@stof

As part of the on-going effort to become spec-compliant and to make it easier to implement modern sass features in the future, I'm convinced we need to modernize the architecture of the code to make it easier to work on it. I'm also convinced that we should move towards an object-oriented codebase instead of all these array($type, $content, $extra) structures, as that will help a lot (especially given that the current array shapes are different for each type).

In order to make it easier to implement modern sass features, looking at the architecture of dart-sass makes sense IMO. The closer we are, the easier it would be to port new features. Here is the high-level parts of the dart-sass architecture:

  • Value value objects (one sub-class per value type) to represent Sass values (equivalent to what zval represents in PHP for instance)
  • Ast\Sass to represent the Sass AST
  • Ast\Css to represent a CSS AST
  • Parser which takes an input (string) and produces a Sass AST (they actually have 3 parsers, one for Scss, one for Sass and one for CSS which handles forbidding most Sass syntaxes when importing CSS files)
  • Evaluator which walks the Sass AST to produce a CSS AST
  • Serializer which turns the CSS AST into a string (the output)
  • Environment which handles all the state of the evaluator (scopes, defined functions, modules, etc...)
  • Compiler which is the public API, which does nothing except calling the Parser then the Evaluator and then the Serializer

Here is the current architecture of scssphp:

  • the array($type, $content, $extra) shapes are representing both the Sass AST and Sass values, without a clear distinction. The Number object is also part of that, which is used to represent the number values, but still needs to support ArrayAccess reads to be part of the AST
  • \ScssPhp\ScssPhp\Formatter\OutputBlock is what comes closer to the CSS AST, except it has no explicit knowledge about the structure of CSS
  • \ScssPhp\ScssPhp\Parser is the scss and css parser (we don't support the indented sass syntax)
  • \ScssPhp\ScssPhp\Formatter (and its subclasses) is what comes closer to the dart-sass Serializer, except that it is a lot harder to understand due to the weird representation it operates on
  • \ScssPhp\ScssPhp\Compiler is the public API, the evaluator, the implementation of builtin APIs, part of the parser (forbidding @return outside functions is implemented in the compiler in Scssphp rather than in the parser for instance) and parts of the serializer
  • \ScssPhp\ScssPhp\Compiler\Environment handles the state (but as a single scope, which creates conflicts between names of mixins, functions and variables, which is not spec-compliant AFAIK)

Long term, I think having typed values, typed Sass AST and typed CSS AST (being the input of serializers/formatters) would make sense. But this is a huge rewrite. As such, I suggest an incremental process:

  1. Separate values and AST in our array($type, $content, $extra) structure
  2. Migrate to object-oriented values
  3. (later) Rework the Sass AST
  4. (even later) Rework the evaluation and serialization to use a CSS AST instead of \ScssPhp\ScssPhp\Formatter\OutputBlock

Separation of values and AST

AST nodes actually belong to 2 families:

  • structural nodes (at-rules, statements, rules, variable declarations, etc...)
  • expressions (which appear in many places inside structural nodes, but can only contain expression nodes as children)

I suggest to segment our type values into 3 categories:

  • structure nodes
  • expression nodes
  • values

This will require introducing new types for cases which are ambiguous today:

  • a ValueExpression to wrap places where we have a basic value in the AST of an expression
  • distinguish map values from map expressions (which evaluate to a map value)
  • distinguish list values from list expressions (which evaluate to a map value)
  • distinguish sass string values from string expressions (which evaluate to a sass string after resolving interpolation)

We would rework our evaluation of expressions to clearly distinguish places dealing with an expression or a value (Compiler::reduce is basically evaluateExpression today). Methods would either take a node as argument or a value, but not both.

Object-oriented values

Once values and nodes are treated separately, we can migrate a proper object-oriented API for values instead of using arrays.

I suggest using the Value subnamespace for them (instead of Node used today for Number, as they are not nodes of the AST). All these classes would extend from a base Value type which will declare some base API common to all types, similar to what dart-sass does. All these value objects will be immutable.

Here is my proposal, which correspond to types available in dart-sass:

  • SassNumber (the existing Number object renamed)
  • SassBoolean
  • SassNull
  • SassList
  • SassMap
  • SassColor
  • SassFunction (returned by get-function)
  • SassArgumentList extends SassList (as argument lists must also handle keyword arguments)
  • SassString represents both quoted and unquoted string, which includes identifiers, as dart-sass found out that it makes it a lot easier to implement string functions like that.

These classes will not implement ArrayAccess anymore.

Arguments to keep the Sass prefix in class names:

  • boolean, null, list, function and string are reserved keywords in PHP and so cannot be used without prefix
  • it makes it clear that they represent values from the Sass language, and so are subject to the Sass rules (for instance, an empty string is truthy in Sass). This makes it easier when we will deal with PHP types and Sass values in the code.

@Cerdic does this plan looks good to you ?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions