pulumi/pkg/codegen/hcl2
Will Jones b859796754
Add more documentation on PCL (#18193)
PCL, or Pulumi Configuration Language, is the core intermediate language
that allows several parts of Pulumi to work in a language-agnostic
manner. This change documents some of the processes and functionality
that we expose for working with PCL programs, incorporating some
existing documentation on extensions we make to e.g. HCL's type system.
2025-01-09 10:18:08 +00:00
..
model Add more documentation on PCL (#18193) 2025-01-09 10:18:08 +00:00
syntax Enable goheader rule and add missing license headers (#15473) 2024-09-09 12:05:45 +00:00
README.md Add more documentation on PCL (#18193) 2025-01-09 10:18:08 +00:00

README.md

PCL syntax and type system

PCL's syntax is a subset of that expressible using HCL. Its type system is largely an extension of the HCL syntax-agnostic information model. The pkg/codegen/hcl2 package exposes code for working with PCL programs at the level of parsed syntax and type-checked models.

(pcl-lexing-parsing)=

Lexing and parsing

The pkg/codegen/hcl2/syntax package exports types and functions for parsing PCL source into an abstract syntax tree (AST) using the existing hclsyntax package. At a high level, a PCL program's syntax tree is structured as follows:

  • A program consists of a set of Files. Each file corresponds to some source file, and has a body representing that file's content.
  • A body comprises any number of attributes and blocks.
  • A block has a type and a (possibly empty) set of labels, as well as a body of its own.
  • An attribute has a name and an associated expression.
  • An expression may be any of a number of supported kinds of expressions (literals such as strings and numbers; unary and binary operations over child expressions such as negation, addition, etc.; references to other attributes or blocks; and so on).

As an example, suppose we have a single file, main.pp, with the following content:

resource "r" "pkg:index:Resource" {
    name = "r"
}

x = { r = r }

output "o" {
    value = r.name
}

The syntax tree for this program would look as follows:

  • A file named main.pp, with a body containing the following children:
    • A block of type resource with a list of labels comprising the strings "r" and "pkg:index:Resource", and a body containing the following children:
      • An attribute named name with an expression representing the string literal "r".
    • An attribute named x with an expression representing an object literal with a single key-value pair, mapping the key r to the expression r.
    • A block of type output with a list of labels comprising the string "o", and a body containing the following children:
      • An attribute named value with the expression r.name.

At the syntactic level, a PCL program is no more than a piece of HCL syntax. Notions of "resources", "outputs", and so on, do not exist. In order to, for instance, assign semantics to a piece of PCL syntax (e.g. "the expression r.name refers to the name attribute of the r resource"), or to decide whether or not some PCL program is in some sense "well-formed", we need to bind the program to produce a semantic model. The part of binding that can be performed at the level of the syntax tree itself (that is, without understanding higher-level concepts such as resources or outputs) is known as type checking.

(pcl-type-checking)=

Type checking

The pkg/codegen/hcl2/model package exports types and functions for type checking parsed HCL syntax to produce a semantic model. Specifically, it provides functionality for type checking expressions, since these are the only parts of the syntax that can be reasoned about without coupling the syntax to higher-level concepts such as resources and outputs. Type checking thus comprises a part of the wider binding process, whereby a program that is being bound is type checked, with the type checker receiving as input a scope that helps it reason about the types of e.g. references it encounters in expressions. In this way, a reference can be understood to e.g. have an object type without having to couple the type checker to how a resource's type is an object consisting of its output properties.

This package's types mirror (and typically wrap) the types of the AST, adding semantic information such as types and resolved references. So for instance, a model.LiteralValueExpression, which corresponds to a literal value such as the string "foo" or the number 42, contains:

  • a reference to the hclsyntax.LiteralValueExpr underpinning it in the syntax tree;
  • an evaluated cty.Value representing the value of the literal; and
  • a model.Type representing the type of the literal.

(pcl-type-system)=

Type system

This section covers the aforementioned extensions PCL's type system makes to the HCL syntax-agnostic information model.

Types

Primitive types

PCL adds the int primitive type. An int is an arbitrary-precision integer value. Implementations must make full-precision values available to consumer applications for interpretation into any suitable integer representation. Implementations may in practice implement ints with limited precision so long as the following constraints are met:

  • Integers are represented with at least 256 bits.
  • An error is produced if an integer value given in source cannot be represented precisely.

Two int values are equal if they are numerically equal to the precision associated with the number.

Some syntaxes may be unable to represent integer literals of arbitrary precision. This must be defined in the syntax specification as part of its description of mapping numeric literals to HCL values.

Structural types

PCL adds union types as a kind of structural type. A union type is constructed of a set of types, and is assignable from any type that is assignable to one of its element types.

A union type is traversed by traversing each of its element types. The result of the traversal is the union of the results of the traversals that succeed. When traversing a union with an element type of none, the traversal of none successfully results in none; this allows a traversal of an optional value to return an optional value of the appropriate type.

Eventual types

PCL adds two eventual type kinds, promise and output. These types represent values that are only available asynchronously, and can be used by applications that produce such values to more accurately track which values are available promptly and which are not.

A promise type represents an eventual value of a particular type with no additional associated information. A promise type is assignable from itself or from its element type. Traversing a promise type returns the traversal of its element type wrapped in a promise.

An output type represents an eventual value of a particular type that carries additional application-specific information. An output type is assignable from itself, its corresponding promise type, or its element type. Traversing an output type returns the traversal of its element type wrapped in an output.

Null values and none

PCL includes a first-class representation for the null value, the none type. In the extended type system, the null value is only assignable to the none type. Optional values of type T are represented by a union of T and none.

Conversions

Primitive type conversions

Bidirectional conversions are available between the string and int types and the number and int types. Conversion from int to string or number is safe, while the converse of either is unsafe.

Collection and structural type conversions

Conversion from a type T to a union type is permitted if there is a conversion from T to at least one of the union's element types. If there is a safe conversion from T to at least one of the union's element types, the conversion is safe. Otherwise, the conversion is unsafe.

Eventual type conversions

Conversion from a type T to a promise with element type U is permitted if T is a promise with element type V where V is convertible to U or if T is convertible to U. The safety of this conversion depends on the safety of the conversion from V or T to U.

Conversion from a type T to an output with element type U is permitted if T is an output or promise with element type V where V is convertible to U or if T is convertible to U. The safety of this conversion depends on the safety of the conversion from V or T to U.

Unification

The int type unifies with number by preferring number, and unifies with string by preferring string.

Two union types unify by producing a new union type whose elements are the concatenation of those of the two input types.

A union type unifies with another type by producing a new union whose element types are the unification of the other type with each of the input union's element types.

A promise type unifies with an output type by producing a new output type whose element type is the unification of the output type's element type and the promise type's element types.

Two promise types unify by producing a new promise type whose element type is the unification of the element types of the two promise types.

Two output types unify by producing a new output type whose element type is the unification of the element types of the two output types.