Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/changes/@typespec/compiler/decimal_2023-05-18-23-22.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/compiler",
"comment": "Add decimal and decimal128 built-in scalar types.",
"type": "none"
}
],
"packageName": "@typespec/compiler"
}
10 changes: 10 additions & 0 deletions common/changes/@typespec/openapi3/decimal_2023-05-18-23-22.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/openapi3",
"comment": "Support decimal and decimal128 scalar types.",
"type": "none"
}
],
"packageName": "@typespec/openapi3"
}
10 changes: 10 additions & 0 deletions common/changes/@typespec/protobuf/decimal_2023-05-23-00-16.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/protobuf",
"comment": "",
"type": "none"
}
],
"packageName": "@typespec/protobuf"
}
34 changes: 18 additions & 16 deletions docs/language-basics/built-in-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@ Built in types are related to each other according to the rules described in [ty

## Numeric types

| Type | Range | Description |
| --------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------- |
| `numeric` | | Parent type for all numeric types |
| `integer` | | A whole-number |
| `float` | | A number with decimal value |
| `int64` | `-9,223,372,036,854,775,808` to `9,223,372,036,854,775,807` | A 64-bit integer |
| `int32` | `-2,147,483,648` to `2,147,483,647` | A 32-bit integer |
| `int16` | `-32,768` to `32,767` | A 16-bit integer |
| `int8` | `-128` to `127` | A 8-bit integer |
| `safeint` | <code>−9007199254740991 (−(2<sup>53</sup> − 1))</code> to <code>9007199254740991 (2<sup>53</sup> − 1)</code> | An integer that can be serialized to JSON |
| `uint64` | `0` to `18,446,744,073,709,551,615` | Unsigned 64-bit integer |
| `uint32` | `0` to `4,294,967,295` | Unsigned 32-bit integer |
| `uint16` | `0` to `65,535` | Unsigned 16-bit integer |
| `uint8` | `0` to `255 ` | Unsigned 8-bit integer |
| `float32` | <code> ±1.5 x 10<sup>45</sup></code> to <code>±3.4 x 10<sup>38</sup></code> | A 32 bit floating point number |
| `float64` | <code>±5.0 × 10<sup>−324</sup></code> to <code>±1.7 × 10<sup>308</sup></code> | A 64 bit floating point number |
| Type | Range | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------- |
| `numeric` | | Parent type for all numeric types |
| `integer` | | A whole-number |
| `float` | | A binary number |
| `int64` | `-9,223,372,036,854,775,808` to `9,223,372,036,854,775,807` | A 64-bit integer |
| `int32` | `-2,147,483,648` to `2,147,483,647` | A 32-bit integer |
| `int16` | `-32,768` to `32,767` | A 16-bit integer |
| `int8` | `-128` to `127` | A 8-bit integer |
| `safeint` | <code>−9007199254740991 (−(2<sup>53</sup> − 1))</code> to <code>9007199254740991 (2<sup>53</sup> − 1)</code> | An integer that can be serialized to JSON |
| `uint64` | `0` to `18,446,744,073,709,551,615` | Unsigned 64-bit integer |
| `uint32` | `0` to `4,294,967,295` | Unsigned 32-bit integer |
| `uint16` | `0` to `65,535` | Unsigned 16-bit integer |
| `uint8` | `0` to `255 ` | Unsigned 8-bit integer |
| `float32` | <code> ±1.5 x 10<sup>45</sup></code> to <code>±3.4 x 10<sup>38</sup></code> | A 32 bit floating point number |
| `float64` | <code>±5.0 × 10<sup>−324</sup></code> to <code>±1.7 × 10<sup>308</sup></code> | A 64 bit floating point number |
| `decimal` | | A decimal number |
Comment thread
bterlson marked this conversation as resolved.
| `decimal128` | 34 decimal digits with an exponent range from `-6143` to `6144` | A 128 bit decimal number |

## Date and time types

Expand Down
2 changes: 1 addition & 1 deletion docs/language-basics/type-literals.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ two

## Numeric literal

Numeric literals can be declare by using the raw number (`interger` or `float`)
Numeric literals can be declared by using the raw number

```typespec
alias Kilo = 1000;
Expand Down
2 changes: 2 additions & 0 deletions docs/language-basics/type-relations.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ graph RL
float --> numeric
float32 --> float
float64 --> float
decimal --> numeric
decimal128 --> decimal
end
string --> unknown
boolean --> unknown
Expand Down
14 changes: 14 additions & 0 deletions packages/compiler/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4472,9 +4472,20 @@ export function createChecker(program: Program): Checker {
type = createType({ kind: "Boolean", value });
break;
case "number":
let valueAsString: string;
if (node) {
compilerAssert(
node.kind === SyntaxKind.NumericLiteral,
"Must pass numeric literal node or undefined when creating a numeric literal type"
);
valueAsString = node.valueAsString;
} else {
valueAsString = String(value);
}
type = createType({
kind: "Number",
value,
valueAsString,
});
break;
}
Expand Down Expand Up @@ -4775,6 +4786,9 @@ export function createChecker(program: Program): Checker {
}

if (target.name === "numeric") return true;
if (target.name === "decimal") return true;
if (target.name === "decimal128") return true;

const isInt = Number.isInteger(source.value);
if (target.name === "integer") return isInt;
if (target.name === "float") return true;
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1354,13 +1354,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa

function parseNumericLiteral(): NumericLiteralNode {
const pos = tokenPos();
const text = tokenValue();
const value = Number(text);
const valueAsString = tokenValue();
const value = Number(valueAsString);

parseExpected(Token.NumericLiteral);
return {
kind: SyntaxKind.NumericLiteral,
value,
valueAsString,
...finishNode(pos),
};
}
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ export type IntrinsicScalarName =
| "safeint"
| "float32"
| "float64"
| "decimal"
| "decimal128"
| "string"
| "plainDate"
| "plainTime"
Expand Down Expand Up @@ -454,6 +456,7 @@ export interface NumericLiteral extends BaseType {
kind: "Number";
node?: NumericLiteralNode;
value: number;
valueAsString: string;
}

export interface BooleanLiteral extends BaseType {
Expand Down Expand Up @@ -1204,6 +1207,7 @@ export interface StringLiteralNode extends BaseNode {
export interface NumericLiteralNode extends BaseNode {
readonly kind: SyntaxKind.NumericLiteral;
readonly value: number;
readonly valueAsString: string;
}

export interface BooleanLiteralNode extends BaseNode {
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler/lib/lib.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ scalar float64 extends float;
*/
scalar float32 extends float64;

/**
* A decimal number with any length and precision.
*/
scalar decimal extends numeric;

/**
* A 128-bit decimal number.
*/
scalar decimal128 extends decimal;

/**
* A sequence of textual characters.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/test/checker/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe("compiler: models", () => {
["boolean", `false`, { kind: "Boolean", value: false, isFinished: false }],
["boolean", `true`, { kind: "Boolean", value: true, isFinished: false }],
["string", `"foo"`, { kind: "String", value: "foo", isFinished: false }],
["int32", `123`, { kind: "Number", value: 123, isFinished: false }],
["int32", `123`, { kind: "Number", value: 123, valueAsString: "123", isFinished: false }],
];

for (const [type, defaultValue, expectedValue] of testCases) {
Expand Down
29 changes: 29 additions & 0 deletions packages/compiler/test/checker/relation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ describe("compiler: checker: type relations", () => {
"uint16",
"uint32",
"uint64",
"decimal",
"decimal128",
"string",
"numeric",
"float",
Expand Down Expand Up @@ -420,6 +422,8 @@ describe("compiler: checker: type relations", () => {
"float",
"float32",
"float64",
"decimal",
"decimal128",
].forEach((x) => {
it(`can assign ${x}`, async () => {
await expectTypeAssignable({ source: x, target: "numeric" });
Expand All @@ -446,6 +450,31 @@ describe("compiler: checker: type relations", () => {
});
});

describe("decimal target", () => {
it("can assign decimal", async () => {
await expectTypeAssignable({ source: "decimal", target: "decimal" });
});
it("can assign decimal128", async () => {
await expectTypeAssignable({ source: "decimal128", target: "decimal" });
});
it("can assign numeric literals", async () => {
await expectTypeAssignable({ source: "-2147483448", target: "decimal" });
Comment thread
bterlson marked this conversation as resolved.
await expectTypeAssignable({ source: "2147483448", target: "decimal" });
await expectTypeAssignable({ source: "2147483448.12390812", target: "decimal" });
});
});

describe("decimal128 target", () => {
it("can assign decimal128", async () => {
await expectTypeAssignable({ source: "decimal128", target: "decimal128" });
});
it("can assign numeric literals", async () => {
await expectTypeAssignable({ source: "-2147483448", target: "decimal128" });
await expectTypeAssignable({ source: "2147483448", target: "decimal128" });
await expectTypeAssignable({ source: "2147483448.12390812", target: "decimal128" });
});
});

describe("object target", () => {
["object", "Record<string>", "Record<int32>"].forEach((x) => {
it(`can assign ${x}`, async () => {
Expand Down
14 changes: 14 additions & 0 deletions packages/compiler/test/checker/scalar.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ok, strictEqual } from "assert";
import { Model, NumericLiteral } from "../../core/index.js";
import {
BasicTestRunner,
createTestHost,
Expand Down Expand Up @@ -62,6 +63,19 @@ describe("compiler: scalars", () => {
strictEqual(B.kind, "Scalar" as const);
});

it("allows a decimal to have a default value", async () => {
const { A } = (await runner.compile(`
@test model A {
x: decimal = 42;
}
`)) as { A: Model };

const def = A.properties.get("x")!.default! as NumericLiteral;
strictEqual(def.kind, "Number" as const);
strictEqual(def.value, 42);
strictEqual(def.valueAsString, "42");
});

describe("custom scalars and default values", () => {
it("allows custom numeric scalar to have a default value", async () => {
const { S, M } = await runner.compile(`
Expand Down
63 changes: 51 additions & 12 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
interpolatePath,
IntrinsicScalarName,
IntrinsicType,
isArrayModelType,
isDeprecated,
isErrorType,
isGlobalNamespace,
Expand Down Expand Up @@ -337,7 +338,7 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
}

const variable: OpenAPI3ServerVariable = {
default: prop.default ? getDefaultValue(prop.default) : "",
default: prop.default ? getDefaultValue(prop.type, prop.default) : "",
description: getDoc(program, prop),
};

Expand Down Expand Up @@ -1104,7 +1105,7 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
}
const schema = applyIntrinsicDecorators(param, typeSchema);
if (param.default) {
schema.default = getDefaultValue(param.default);
schema.default = getDefaultValue(param.type, param.default);
}
// Description is already provided in the parameter itself.
delete schema.description;
Expand Down Expand Up @@ -1440,25 +1441,59 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
return type.kind === "Boolean" || type.kind === "String" || type.kind === "Number";
}

function getDefaultValue(type: Type): any {
switch (type.kind) {
function getDefaultValue(type: Type, defaultType: Type): any {
switch (defaultType.kind) {
case "String":
return type.value;
return defaultType.value;
case "Number":
return type.value;
compilerAssert(type.kind === "Scalar", "setting scalar default to non-scalar value");
const base = getStdBaseScalar(type);
compilerAssert(base, "not allowed to assign default to custom scalars");

if (base.name === "decimal" || base.name === "decimal128") {
return defaultType.valueAsString;
}

return defaultType.value;
case "Boolean":
return type.value;
return defaultType.value;
case "Tuple":
return type.values.map(getDefaultValue);
compilerAssert(
type.kind === "Tuple" || (type.kind === "Model" && isArrayModelType(program, type)),
"setting tuple default to non-tuple value"
);

if (type.kind === "Tuple") {
return defaultType.values.map((defaultTupleValue, index) =>
getDefaultValue(type.values[index], defaultTupleValue)
);
} else {
return defaultType.values.map((defaultTuplevalue) =>
getDefaultValue(type.indexer!.value, defaultTuplevalue)
);
}

case "EnumMember":
return type.value ?? type.name;
return defaultType.value ?? defaultType.name;
default:
reportDiagnostic(program, {
code: "invalid-default",
format: { type: type.kind },
target: type,
format: { type: defaultType.kind },
target: defaultType,
});
}

function getStdBaseScalar(scalar: Scalar): Scalar | null {
let current: Scalar | undefined = scalar;
while (current) {
if (program.checker.isStdType(current)) {
return current;
}
current = current.baseScalar;
}

return null;
}
}

function includeDerivedModel(model: Model): boolean {
Expand Down Expand Up @@ -1563,7 +1598,7 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
}

if (prop.default) {
additionalProps.default = getDefaultValue(prop.default);
additionalProps.default = getDefaultValue(prop.type, prop.default);
}

if (isReadonlyProperty(program, prop)) {
Expand Down Expand Up @@ -1844,6 +1879,10 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt
return { type: "number", format: "double" };
case "float32":
return { type: "number", format: "float" };
case "decimal":
return { type: "string", format: "decimal" };
case "decimal128":
return { type: "string", format: "decimal128" };
case "string":
return { type: "string" };
case "boolean":
Expand Down
Loading