Skip to content

Commit

Permalink
Value world, object, tuple literals, const (#3022)
Browse files Browse the repository at this point in the history
resolves #2046
[Playround](https://cadlplayground.z22.web.core.windows.net/prs/3022/)

Add the new syntax for object literals using `#{`. For this first
version an object literal can only contain other object literal and
other literals(string, number, boolean))

## Values axioms
1. `alias` always produces a type. If you attempt to alias a value, you
get an error.
2. A string template produces a string template type if all
substitutions are types, and a value if all substitutions are numeric,
boolean, or string values. A mixture of types and values is an error.
3. The string literal syntax always results in a string literal type
4. A string literal type may be passed as a string value when the
signature expects a value. When the signature expects either a string
literal type or a string value, it is passed as a string value.
5. A string template type can be passed as a string value when all its
substitutions are string literal types.

## Breaking change

### Removal of the `ValueType` replacement with `MixedConstraint`

This shouldn't affect anyone as you were only exposed to this if you
digged into the template parameter and looked at the constraint

## Deprecation

## Using a tuple instead of a tuple literal
- ✅ still work
- emit a warning
<img width="1013" alt="image"
src="https://github.com/microsoft/typespec/assets/1031227/ab05359a-5ed9-4a27-a8d1-f40d1e21766f">

- provide a codefix
<img width="312" alt="image"
src="https://github.com/microsoft/typespec/assets/1031227/5ef93bdf-665f-4445-a6b2-62475efe8c16">

## Using a model expression instead of an object literal
This technically didn't work before(different from above where tuple was
used as a value) but allow this will allow us to convert most of our
decorators to use `valueof` without being breaking
![Kapture 2024-03-18 at 19 31
32](https://github.com/microsoft/typespec/assets/1031227/f6d69ab4-139e-4b01-95a3-f376b8515d1c)

## Old decorator marshalling

If a library had a decorator with `valueof` one of those types
`numeric`, `int64`, `uint64`, `integer`, `float`, `decimal`,
`decimal128`, `null` it used to marshall those as JS `number` and
`NullType` for `null`. With the introduction of values we have a new
marshalling logic which will marshall those numeric types as `Numeric`
and the others will remain numbers. `null` will also get marshalled as
`null`.

For now this is an opt-in behavior with a warning on decorators not
opt-in having a parameter with a constraint from the list above.

Example: 
```
extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric);
```
Will now emit a deprecated warning because `value` is of type `valueof
string` which would marshall to `Numeric` under the new logic but as
`number` previously.

To opt-in you can add the following to your library 
```ts
export const $flags = defineModuleFlags({
  decoratorArgMarshalling: "value",
});
```

---------

Co-authored-by: Brian Terlson <brian.terlson@microsoft.com>
Co-authored-by: Mark Cowlishaw <markcowl@microsoft.com>
  • Loading branch information
3 people authored May 8, 2024
1 parent dec5043 commit 7ec1716
Show file tree
Hide file tree
Showing 122 changed files with 8,055 additions and 1,237 deletions.
17 changes: 17 additions & 0 deletions .chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: deprecation
packages:
- "@typespec/compiler"
---

Using a tuple type as a value is deprecated. Tuple types in contexts where values are expected must be updated to be array values instead. A codefix is provided to automatically convert tuple types into array values.

```tsp
model Test {
// Deprecated
values: string[] = ["a", "b", "c"];
// Correct
values: string[] = #["a", "b", "c"];
```
17 changes: 17 additions & 0 deletions .chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: deprecation
packages:
- "@typespec/compiler"
---

Using a model type as a value is deprecated. Model types in contexts where values are expected must be updated to be object values instead. A codefix is provided to automatically convert model types into object values.

```tsp
model Test {
// Deprecated
user: {name: string} = {name: "System"};
// Correct
user: {name: string} = #{name: "System"};
```
30 changes: 30 additions & 0 deletions .chronus/changes/feature-object-literals-2024-2-15-18-36-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/compiler"
---

Add syntax for declaring values. [See docs](https://typespec.io/docs/language-basics/values).

Object and array values
```tsp
@dummy(#{
name: "John",
age: 48,
address: #{ city: "London" }
aliases: #["Bob", "Frank"]
})
```

Scalar constructors

```tsp
scalar utcDateTime {
init fromISO(value: string);
}
model DateRange {
minDate: utcDateTime = utcDateTime.fromISO("2024-02-15T18:36:03Z");
}
```
10 changes: 10 additions & 0 deletions .chronus/changes/feature-object-literals-2024-2-18-22-23-26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/json-schema"
- "@typespec/protobuf"
- "@typespec/versioning"
---

Update to support new value types
23 changes: 23 additions & 0 deletions .chronus/changes/feature-object-literals-2024-3-16-10-38-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
changeKind: deprecation
packages:
- "@typespec/compiler"
---

Decorator API: Legacy marshalling logic

With the introduction of values, the decorator marshalling behavior has changed in some cases. This behavior is opt-in by setting the `valueMarshalling` package flag to `"new"`, but will be the default behavior in future versions. It is strongly recommended to adopt this new behavior as soon as possible.


Example:
```tsp
extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric);
```
Will now emit a deprecated warning because `value` is of type `valueof string` which would marshall to `Numeric` under the new logic but as `number` previously.

To opt-in you can add the following to your library js/ts files.
```ts
export const $flags = definePackageFlags({
decoratorArgMarshalling: "new",
});
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/openapi3"
---

Add support for new object and array values as default values (e.g. `decimals: decimal[] = #[123, 456.7];`)
7 changes: 7 additions & 0 deletions .chronus/changes/feature-object-literals-2024-3-16-12-0-15.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/rest"
---

Update types to support new values in TypeSpec
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: feature
packages:
- "@typespec/html-program-viewer"
---

Add support for values
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/http"
---

Update Flow Template to make use of the new array values

20 changes: 16 additions & 4 deletions docs/extending-typespec/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Open `./src/lib.ts` and create your library definition that registers your libra
If `$lib` is not accessible from your library package (for example, `import {$lib} from "my-library";`), some features such as linting and emitter option validation will not be available.
:::

Here's an example:
For example:

```typescript
import { createTypeSpecLibrary } from "@typespec/compiler";
Expand All @@ -122,7 +122,19 @@ export const { reportDiagnostic, createDiagnostic } = $lib;

Diagnostics are used for linters and decorators, which are covered in subsequent topics.

### f. Create `index.ts`
### f. Set package flags

You can optionally set any package flags by exporting a `$flags` const that is initialized with the `definePackageFlags`. Like `$lib`, this value must be exported from your package.

It is strongly recommended to set `valueMarshalling` to `"new"` as this will be the default behavior in future TypeSpec versions.

```typescript
export const $flags = definePackageFlags({
valueMarshalling: "new",
});
```

### g. Create `index.ts`

Open `./src/index.ts` and import your library definition:

Expand All @@ -131,7 +143,7 @@ Open `./src/index.ts` and import your library definition:
export { $lib } from "./lib.js";
```

### g. Build TypeScript
### h. Build TypeScript

TypeSpec can only import JavaScript files, so any changes made to TypeScript sources need to be compiled before they are visible to TypeSpec. To do this, run `npx tsc -p .` in your library's root directory. If you want to re-run the TypeScript compiler whenever files are changed, you can run `npx tsc -p . --watch`.

Expand All @@ -148,7 +160,7 @@ Alternatively, you can add these as scripts in your `package.json` to make them

You can then run `npm run build` or `npm run watch` to build or watch your library.

### h. Add your main TypeSpec file
### i. Add your main TypeSpec file

Open `./lib/main.tsp` and import your JS entrypoint. This ensures that when TypeSpec imports your library, the code to define the library is run. When we add decorators in later topics, this import will ensure those get exposed as well.

Expand Down
70 changes: 38 additions & 32 deletions docs/extending-typespec/create-decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,44 +35,41 @@ using TypeSpec.Reflection;
extern dec track(target: Model | Enum);
```

### Optional parameters
## Optional parameters

You can mark a decorator parameter as optional using `?`.

```typespec
extern dec track(target: Model | Enum, name?: valueof string);
```

### Rest parameters
## Rest parameters

You can prefix the last parameter of a decorator with `...` to collect all the remaining arguments. The type of this parameter must be an `array expression`.

```typespec
extern dec track(target: Model | Enum, ...names: valueof string[]);
```

## Requesting a value type
## Value parameters

It's common for decorator parameters to expect a value (e.g., a string or a number). However, using `: string` as the type would also allow a user of the decorator to pass `string` itself or a custom scalar extending string, as well as a union of strings. Instead, the decorator can use `valueof <T>` to specify that it expects a value of that kind.

| Example | Description |
| ----------------- | ----------------- |
| `valueof string` | Expects a string |
| `valueof float64` | Expects a float |
| `valueof int32` | Expects a number |
| `valueof boolean` | Expects a boolean |
A decorator parameter can receive [values](../language-basics/values.md) by using the `valueof` operator. For example the parameter `valueof string` expects a string value. Values are provided to the decorator implementation according the [decorator parameter marshalling](#decorator-parameter-marshalling) rules.

```tsp
extern dec tag(target: unknown, value: valueof string);
// bad
// error: string is not a value
@tag(string)
// good
@tag("This is the tag name")
// ok, a string literal can be a value
@tag("widgets")
// ok, passing a value from a const
const tagName: string = "widgets";
@tag(tagName)
```

## Implement the decorator in JavaScript
## JavaScript decorator implementation

Decorators can be implemented in JavaScript by prefixing the function name with `$`. A decorator function must have the following parameters:

Expand All @@ -89,7 +86,7 @@ export function $logType(context: DecoratorContext, target: Type, name: valueof
}
```

Or in pure JavaScript:
Or in JavaScript:

```ts
// model.js
Expand All @@ -113,26 +110,35 @@ model Dog {

### Decorator parameter marshalling

For certain TypeSpec types (Literal types), the decorator does not receive the actual type but a marshalled value if the decorator parameter type is a `valueof`. This simplifies the most common cases.
When decorators are passed types, the type is passed as-is. When a decorator is passed a TypeSpec value, the decorator receives a JavaScript value with a type that is appropriate for representing that value.

| TypeSpec Type | Marshalled value in JS |
| ----------------- | ---------------------- |
| `valueof string` | `string` |
| `valueof numeric` | `number` |
| `valueof boolean` | `boolean` |
:::note
This behavior depends on the value of the `valueMarshalling` [package flag](../extending-typespec/basics.md#f-set-package-flags). This section describes the behavior when `valueMarshalling` is set to `"new"`. In a future release this will become the default value marshalling so it is strongly recommended to set this flag. But for now, the default value marshalling is `"legacy"` which is described in the next section. In a future release the `valueMarshalling` flag will need to be set to `"legacy"` to keep the previous marshalling behavior, but the flag will eventually be removed entirely.
:::

For all other types, they are not transformed.
| TypeSpec value type | Marshalled type in JS |
| ------------------- | --------------------------------- |
| `string` | `string` |
| `boolean` | `boolean` |
| `numeric` | `Numeric` or `number` (see below) |
| `null` | `null` |
| enum member | `EnumMemberValue` |

Example:
When marshalling numeric values, either the `Numeric` wrapper type is used, or a `number` is passed directly, depending on whether the value can be represented as a JavaScript number without precision loss. In particular, the types `numeric`, `integer`, `decimal`, `float`, `int64`, `uint64`, and `decimal128` are marshalled as a `Numeric` type. All other numeric types are marshalled as `number`.

```ts
export function $tag(
context: DecoratorContext,
target: Type,
stringArg: string, // Here instead of receiving a `StringLiteral`, the string value is being sent.
modelArg: Model // Model has no special handling so we receive the Model type
) {}
```
When marshalling custom scalar subtypes, the marshalling behavior of the known supertype is used. For example, a `scalar customScalar extends numeric` will marshal as a `Numeric`, regardless of any value constraints that might be present.

#### Legacy value marshalling

With legacy value marshalling, TypeSpec strings, numbers, and booleans values are always marshalled as JS values. All other values are marshalled as their corresponding type. For example, `null` is marshalled as `NullType`.

| TypeSpec Value Type | Marshalled value in JS |
| ------------------- | ---------------------- |
| `string` | `string` |
| `numeric` | `number` |
| `boolean` | `boolean` |

Note that with legacy marshalling, because JavaScript numbers have limited range and precision, it is possible to define values in TypeSpec that cannot be accurately represented in JavaScript.

#### String templates and marshalling

Expand Down
2 changes: 1 addition & 1 deletion docs/language-basics/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ model Dog {

## Creating decorators

_For more information on creating decorators, see the [Creating Decorators Documentation](../extending-typespec/create-decorators.md)._
For more information on creating decorators, see [Creating Decorators](../extending-typespec/create-decorators.md).
16 changes: 15 additions & 1 deletion docs/language-basics/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,23 @@ scalar Password extends string;

## Scalars with template parameters

Scalars can also support template parameters. However, it's important to note that these templates are primarily used for decorators.
Scalars can also support template parameters. These template parameters are primarily used for decorators.

```typespec
@doc(Type)
scalar Unreal<Type extends string>;
```

## Scalar initializers

Scalars can be declared with an initializer for creating specific scalar values based on other values. For example:

```typespec
scalar ipv4 extends string {
init fromInt(value: uint32);
}
const homeIp = ipv4.fromInt(2130706433);
```

Initializers do not have any runtime code associated with them. Instead, they merely record the scalar initializer invoked along with the arguments passed so that emitters can construct the proper value when needed.
40 changes: 40 additions & 0 deletions docs/language-basics/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,43 @@ alias Example3 = Test<
Since template arguments can be specified by name, the names of template parameters are part of the template's public API. **Renaming a template parameter may break existing specifications that use the template.**

**Note**: Template arguments are evaluated in the order the parameters are defined in the template _definition_, not the order in which they are written in the template _instance_. While this is usually inconsequential, it may be important in some cases where evaluating a template argument may trigger decorators with side effects.

## Templates with values

Templates can be declared to accept values using a `valueof` constraint. This is useful for providing default values and parameters for decorators that take values.

```typespec
alias TakesValue<StringType extends string, StringValue extends valueof string> = {
@doc(StringValue)
property: StringType;
};
alias M1 = TakesValue<"a", "b">;
```

When a passing a literal or an enum or union member reference directly as a template parameter that accepts either a type or a value, we pass the value. In particular, `StringTypeOrValue` is a value with the string literal type `"a"`.

```typespec
alias TakesTypeOrValue<StringTypeOrValue extends string | (valueof string)> = {
@customDecorator(StringOrValue)
property: string;
};
alias M1 = TakesValue<"a">;
```

The [`typeof` operator](./values.md#the-typeof-operator) can be used to get the declared type of a value if needed.

### Template parameter value types

When a template is instantiated with a value, the type of the value and the result of the `typeof` operator is determined based on the argument rather than the template parameter constraint. This follows the same rules as [const declaration type inference](./values.md#const-declarations). In particular, inside the template `TakesValue`, the type of `StringValue` is the string literal type `"b"`. If we passed a `const` instead, the type of the value would be the const's type. In the following example, the type of `property` in `M1` is `"a" | "b"`.

```typespec
alias TakesValue<StringValue extends valueof string> = {
@doc(StringValue)
property: typeof StringValue;
};
const str: "a" | "b" = "a";
alias M1 = TakesValue<str>;
```
Loading

0 comments on commit 7ec1716

Please sign in to comment.