Skip to content

Values as typed objects #265

@stof

Description

@stof

This issue serves as a detailed spec for the refactoring of the representation of values for #193

Design of value objects

Value objects are immutable. Their API is modelled after the dart-sass classes for values, with a few differences:

  • we don't distinguish between an internal implementation and an external type. This is a "hack" used in dart-sass to workaround the absence of compiler-enforced internal APIs in dart (which will be solved in the next version of dart AFAIK so dart-sass will probably change that in the future anyway). In the PHP ecosystem, IDEs are warning when using methods tagged as @internal. Psalm also reports that as an error during static analysis (for phpstan, there is a feature request). So using @internal will be considered good enough
  • some methods will have slightly different names due to PHP reserved keywords being forbidden in method names on PHP 5.6 (for instance SassList.empty() from dart will become SassList::createEmpty() in PHP)
  • overriding the == operator is not possible, so we'll implement a equals(Value $other): bool method that should be used. that method should be used in any code wanting to compare 2 Sass values
  • methods returning a List<Value> will return a PHP array (which solves easily the fact that dart enforces that returned collections are not modifiable, as PHP array are not passed by reference)
  • for Map<Value, something>, we'll need to find a replacement API. SplObjectStorage is not suited for that as it uses spl_object_hash for equality by default, and its extension point SplObjectStorage::getHash would be very hard to use to implement equality rules (In other languages, like dart for instance, classes have a hashcode API returning an integer. But then, for such equality checks, objects returning the same hashcode are still compared with ==. So the implementation of hashcode can return false positives but not false negatives, which makes things a lot simpler)

Implementation plan

The goal of this plan is to perform an incremental migration of the codebase. This is probably not meant to be released until the end of the migration though, to avoid exposing the migration layer in releases (and then having to keep it for backward compatibility). But that will allow to keep a working 2.0-dev compiler during the migration (and spreading the migration across separate commits/PRs to make it easier).

  1. Implement Value and its subclasses
  2. Add support for Value in the compiler (not reading $value[0] on them, making them no-op in reduce and maybe a few other stuff)
  3. Add support for Value in the formatter (being able to output them) currently, this is handled in Compiler::compileValue actually, the formatter does not deal with our array-based value API but a separate API.
  4. Implement conversion methods in the Compiler to switch between Value and the legacy array-based representation
  5. Implement a way to opt-in for the new API for functions (I'm thinking about a private static variable holding an array of function name => bool, and maybe a way for external functions to opt-in if needed unless we keep that for the end if we never release the migration layer). callNativeFunction will convert the arguments and the return value based on this opt-in. Some rules apply to functions when using the new API:
    • functions must define their prototype (in 1.x, the prototype is optional, and the structure of arguments is different depending on that...)
    • the signature of the callable is function(array<Value> $args): Value
    • when using a rest argument (must be the last one in the prototype), the argument is received as a SassArgumentList (in the position of the rest argument in the prototype), allowing to access both positional and keywords argument
    • when using a rest argument, an error is thrown if the function is called with keyword arguments but the keyword args are never accessed in the SassArgumentList (same than for dart-sass)
  6. Migrate functions to the new API one by one (or more likely by batches of related functions)
  7. Migrate the environment to store variables as values (at that point, the implementation of setVariables changes too)
  8. Migrate the evaluation of expressions to use the new API (can be done in parallel of the migration of functions, by changing the canonical representation in callNativeFunction)
  9. Migrate the parser to parse literal values as Value objects
  10. Remove the conversion layer as the array-based API is used only for AST nodes at that point, not for values

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