diff --git a/common/changes/@typespec/compiler/decimal_2023-05-18-23-22.json b/common/changes/@typespec/compiler/decimal_2023-05-18-23-22.json
new file mode 100644
index 00000000000..37c83c1fc11
--- /dev/null
+++ b/common/changes/@typespec/compiler/decimal_2023-05-18-23-22.json
@@ -0,0 +1,10 @@
+{
+ "changes": [
+ {
+ "packageName": "@typespec/compiler",
+ "comment": "Add decimal and decimal128 built-in scalar types.",
+ "type": "none"
+ }
+ ],
+ "packageName": "@typespec/compiler"
+}
\ No newline at end of file
diff --git a/common/changes/@typespec/openapi3/decimal_2023-05-18-23-22.json b/common/changes/@typespec/openapi3/decimal_2023-05-18-23-22.json
new file mode 100644
index 00000000000..b7112f13668
--- /dev/null
+++ b/common/changes/@typespec/openapi3/decimal_2023-05-18-23-22.json
@@ -0,0 +1,10 @@
+{
+ "changes": [
+ {
+ "packageName": "@typespec/openapi3",
+ "comment": "Support decimal and decimal128 scalar types.",
+ "type": "none"
+ }
+ ],
+ "packageName": "@typespec/openapi3"
+}
\ No newline at end of file
diff --git a/common/changes/@typespec/protobuf/decimal_2023-05-23-00-16.json b/common/changes/@typespec/protobuf/decimal_2023-05-23-00-16.json
new file mode 100644
index 00000000000..8f48e4e9521
--- /dev/null
+++ b/common/changes/@typespec/protobuf/decimal_2023-05-23-00-16.json
@@ -0,0 +1,10 @@
+{
+ "changes": [
+ {
+ "packageName": "@typespec/protobuf",
+ "comment": "",
+ "type": "none"
+ }
+ ],
+ "packageName": "@typespec/protobuf"
+}
\ No newline at end of file
diff --git a/docs/language-basics/built-in-types.md b/docs/language-basics/built-in-types.md
index 350dd2857a3..8da181be28f 100644
--- a/docs/language-basics/built-in-types.md
+++ b/docs/language-basics/built-in-types.md
@@ -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` | −9007199254740991 (−(253 − 1)) to 9007199254740991 (253 − 1) | 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` | ±1.5 x 1045 to ±3.4 x 1038 | A 32 bit floating point number |
-| `float64` | ±5.0 × 10−324 to ±1.7 × 10308 | 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` | −9007199254740991 (−(253 − 1)) to 9007199254740991 (253 − 1) | 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` | ±1.5 x 1045 to ±3.4 x 1038 | A 32 bit floating point number |
+| `float64` | ±5.0 × 10−324 to ±1.7 × 10308 | A 64 bit floating point number |
+| `decimal` | | A decimal number |
+| `decimal128` | 34 decimal digits with an exponent range from `-6143` to `6144` | A 128 bit decimal number |
## Date and time types
diff --git a/docs/language-basics/type-literals.md b/docs/language-basics/type-literals.md
index 7e4edfd0b91..cf6ee9af228 100644
--- a/docs/language-basics/type-literals.md
+++ b/docs/language-basics/type-literals.md
@@ -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;
diff --git a/docs/language-basics/type-relations.md b/docs/language-basics/type-relations.md
index 0315835a363..03f7fca3e49 100644
--- a/docs/language-basics/type-relations.md
+++ b/docs/language-basics/type-relations.md
@@ -29,6 +29,8 @@ graph RL
float --> numeric
float32 --> float
float64 --> float
+ decimal --> numeric
+ decimal128 --> decimal
end
string --> unknown
boolean --> unknown
diff --git a/packages/compiler/core/checker.ts b/packages/compiler/core/checker.ts
index 247e6da1bb5..d4034eefb80 100644
--- a/packages/compiler/core/checker.ts
+++ b/packages/compiler/core/checker.ts
@@ -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;
}
@@ -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;
diff --git a/packages/compiler/core/parser.ts b/packages/compiler/core/parser.ts
index da0536b1eb6..baeb6fc7043 100644
--- a/packages/compiler/core/parser.ts
+++ b/packages/compiler/core/parser.ts
@@ -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),
};
}
diff --git a/packages/compiler/core/types.ts b/packages/compiler/core/types.ts
index ca9a8548703..c68d84e092e 100644
--- a/packages/compiler/core/types.ts
+++ b/packages/compiler/core/types.ts
@@ -184,6 +184,8 @@ export type IntrinsicScalarName =
| "safeint"
| "float32"
| "float64"
+ | "decimal"
+ | "decimal128"
| "string"
| "plainDate"
| "plainTime"
@@ -454,6 +456,7 @@ export interface NumericLiteral extends BaseType {
kind: "Number";
node?: NumericLiteralNode;
value: number;
+ valueAsString: string;
}
export interface BooleanLiteral extends BaseType {
@@ -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 {
diff --git a/packages/compiler/lib/lib.tsp b/packages/compiler/lib/lib.tsp
index 78bc8ef912d..6fa6b8e6b81 100644
--- a/packages/compiler/lib/lib.tsp
+++ b/packages/compiler/lib/lib.tsp
@@ -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.
*/
diff --git a/packages/compiler/test/checker/model.test.ts b/packages/compiler/test/checker/model.test.ts
index 9ed46d68569..0709005e354 100644
--- a/packages/compiler/test/checker/model.test.ts
+++ b/packages/compiler/test/checker/model.test.ts
@@ -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) {
diff --git a/packages/compiler/test/checker/relation.test.ts b/packages/compiler/test/checker/relation.test.ts
index 5cd6297c32e..e7556b04064 100644
--- a/packages/compiler/test/checker/relation.test.ts
+++ b/packages/compiler/test/checker/relation.test.ts
@@ -134,6 +134,8 @@ describe("compiler: checker: type relations", () => {
"uint16",
"uint32",
"uint64",
+ "decimal",
+ "decimal128",
"string",
"numeric",
"float",
@@ -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" });
@@ -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" });
+ 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", "Record"].forEach((x) => {
it(`can assign ${x}`, async () => {
diff --git a/packages/compiler/test/checker/scalar.test.ts b/packages/compiler/test/checker/scalar.test.ts
index facacdca37a..b9763e2809e 100644
--- a/packages/compiler/test/checker/scalar.test.ts
+++ b/packages/compiler/test/checker/scalar.test.ts
@@ -1,4 +1,5 @@
import { ok, strictEqual } from "assert";
+import { Model, NumericLiteral } from "../../core/index.js";
import {
BasicTestRunner,
createTestHost,
@@ -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(`
diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts
index a42006e0771..2ede095ca94 100644
--- a/packages/openapi3/src/openapi.ts
+++ b/packages/openapi3/src/openapi.ts
@@ -31,6 +31,7 @@ import {
interpolatePath,
IntrinsicScalarName,
IntrinsicType,
+ isArrayModelType,
isDeprecated,
isErrorType,
isGlobalNamespace,
@@ -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),
};
@@ -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;
@@ -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 {
@@ -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)) {
@@ -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":
diff --git a/packages/openapi3/test/array.test.ts b/packages/openapi3/test/array.test.ts
index 49bde26932a..2220c9cc5c7 100644
--- a/packages/openapi3/test/array.test.ts
+++ b/packages/openapi3/test/array.test.ts
@@ -76,4 +76,72 @@ describe("openapi3: Array", () => {
"x-typespec-name": "string[]",
});
});
+
+ it("can specify array defaults using tuple syntax", async () => {
+ const res = await oapiForModel(
+ "Pet",
+ `
+ model Pet {
+ names: string[] = ["bismarck"];
+ decimals: decimal[] = [123, 456.7];
+ decimal128s: decimal128[] = [123, 456.7];
+ };
+ `
+ );
+
+ deepStrictEqual(res.schemas.Pet.properties.names, {
+ type: "array",
+ items: { type: "string" },
+ "x-typespec-name": "string[]",
+ default: ["bismarck"],
+ });
+
+ deepStrictEqual(res.schemas.Pet.properties.decimals, {
+ type: "array",
+ items: { type: "string", format: "decimal" },
+ "x-typespec-name": "decimal[]",
+ default: ["123", "456.7"],
+ });
+
+ deepStrictEqual(res.schemas.Pet.properties.decimal128s, {
+ type: "array",
+ items: { type: "string", format: "decimal128" },
+ "x-typespec-name": "decimal128[]",
+ default: ["123", "456.7"],
+ });
+ });
+
+ it("can specify tuple defaults using tuple syntax", async () => {
+ const res = await oapiForModel(
+ "Pet",
+ `
+ model Pet {
+ names: [string, int32] = ["bismarck", 12];
+ decimals: [string, decimal] = ["hi", 456.7];
+ decimal128s: [string, decimal128] = ["hi", 456.7];
+ };
+ `
+ );
+
+ deepStrictEqual(res.schemas.Pet.properties.names, {
+ type: "array",
+ items: {},
+ "x-typespec-name": "[string, int32]",
+ default: ["bismarck", 12],
+ });
+
+ deepStrictEqual(res.schemas.Pet.properties.decimals, {
+ type: "array",
+ items: {},
+ "x-typespec-name": "[string, decimal]",
+ default: ["hi", "456.7"],
+ });
+
+ deepStrictEqual(res.schemas.Pet.properties.decimal128s, {
+ type: "array",
+ items: {},
+ "x-typespec-name": "[string, decimal128]",
+ default: ["hi", "456.7"],
+ });
+ });
});
diff --git a/packages/openapi3/test/primitive-types.test.ts b/packages/openapi3/test/primitive-types.test.ts
index 30ac71340f3..b2e027c740c 100644
--- a/packages/openapi3/test/primitive-types.test.ts
+++ b/packages/openapi3/test/primitive-types.test.ts
@@ -25,6 +25,8 @@ describe("openapi3: primitives", () => {
["plainTime", { type: "string", format: "time" }],
["duration", { type: "string", format: "duration" }],
["bytes", { type: "string", format: "byte" }],
+ ["decimal", { type: "string", format: "decimal" }],
+ ["decimal128", { type: "string", format: "decimal128" }],
];
for (const [name, expectedSchema] of cases) {
diff --git a/packages/protobuf/test/scenarios/array-nested/diagnostics.txt b/packages/protobuf/test/scenarios/array-nested/diagnostics.txt
index 8a512bd0037..179a8fdbf91 100644
--- a/packages/protobuf/test/scenarios/array-nested/diagnostics.txt
+++ b/packages/protobuf/test/scenarios/array-nested/diagnostics.txt
@@ -1 +1 @@
-/test/.tsp/lib/lib.tsp:118:1 - error @typespec/protobuf/nested-array: nested arrays are not supported by the Protobuf emitter
\ No newline at end of file
+/test/.tsp/lib/lib.tsp:128:1 - error @typespec/protobuf/nested-array: nested arrays are not supported by the Protobuf emitter
\ No newline at end of file
diff --git a/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt b/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt
index 1bcc74ba904..ab3800ed9cd 100644
--- a/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt
+++ b/packages/protobuf/test/scenarios/illegal field reservations/diagnostics.txt
@@ -1,2 +1,2 @@
-/test/.tsp/lib/lib.tsp:78:1 - error @typespec/protobuf/illegal-reservation: reservation value must be a string literal, uint32 literal, or a tuple of two uint32 literals denoting a range
+/test/.tsp/lib/lib.tsp:88:1 - error @typespec/protobuf/illegal-reservation: reservation value must be a string literal, uint32 literal, or a tuple of two uint32 literals denoting a range
/test/.tsp/lib/lib.tsp:48:1 - error @typespec/protobuf/illegal-reservation: reservation value must be a string literal, uint32 literal, or a tuple of two uint32 literals denoting a range
\ No newline at end of file