Merge branch 'main' into pierremtb/issue6413-Back-button-on-SignIn-page-leads-to-Home-page
| @ -19,6 +19,11 @@ | ||||
|     "plugin:jsx-a11y/recommended", | ||||
|     "plugin:react-hooks/recommended" | ||||
|   ], | ||||
|   "settings": { | ||||
|     "react": { | ||||
|       "version": "detect" | ||||
|     } | ||||
|   }, | ||||
|   "rules": { | ||||
|     "no-array-constructor": "off", // This is wrong; use the @typescript-eslint one instead. | ||||
|     "@typescript-eslint/no-array-constructor": "error", | ||||
| @ -61,6 +66,7 @@ | ||||
|     "jsx-a11y/click-events-have-key-events": "off", | ||||
|     "jsx-a11y/no-autofocus": "off", | ||||
|     "jsx-a11y/no-noninteractive-element-interactions": "off", | ||||
|     "react/no-unknown-property": "error", | ||||
|     "no-restricted-globals": [ | ||||
|       "error", | ||||
|       { | ||||
|  | ||||
							
								
								
									
										11
									
								
								.github/ci-cd-scripts/playwright-electron.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -26,8 +26,8 @@ max_retries=1 | ||||
| # Retry failed tests, doing our own retries because using inbuilt Playwright retries causes connection issues | ||||
| while [[ $retry -le $max_retries ]]; do | ||||
|     if [[ -f "test-results/.last-run.json" ]]; then | ||||
|         failed_tests=$(jq '.failedTests | length' test-results/.last-run.json) | ||||
|         if [[ $failed_tests -gt 0 ]]; then | ||||
|         status=$(jq -r '.status' test-results/.last-run.json) | ||||
|         if [[ "$status" == "failed" ]]; then | ||||
|             echo "retried=true" >>$GITHUB_OUTPUT | ||||
|             echo "run playwright with last failed tests and retry $retry" | ||||
|             if [[ "$3" == *ubuntu* ]]; then | ||||
| @ -56,10 +56,11 @@ done | ||||
| echo "retried=false" >>$GITHUB_OUTPUT | ||||
|  | ||||
| if [[ -f "test-results/.last-run.json" ]]; then | ||||
|     failed_tests=$(jq '.failedTests | length' test-results/.last-run.json) | ||||
|     if [[ $failed_tests -gt 0 ]]; then | ||||
|         # If it still fails after 3 retries, then fail the job | ||||
|     status=$(jq -r '.status' test-results/.last-run.json) | ||||
|     if [[ "$status" == "failed" ]]; then | ||||
|         # If it still fails after retries, then fail the job | ||||
|         exit 1 | ||||
|     fi | ||||
| fi | ||||
|  | ||||
| exit 0 | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -225,7 +225,7 @@ jobs: | ||||
|         uses: nick-fields/retry@v3.0.2 | ||||
|         with: | ||||
|           shell: bash | ||||
|           command: npm run test:snapshots || true | ||||
|           command: npm run test:snapshots | ||||
|           timeout_minutes: 5 | ||||
|           max_attempts: 5 | ||||
|         env: | ||||
|  | ||||
							
								
								
									
										40
									
								
								docs/kcl/assertIs.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -4,8 +4,12 @@ excerpt: "Converts a number from centimeters to the current default unit." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| **WARNING:** This function is deprecated. | ||||
|  | ||||
| Converts a number from centimeters to the current default unit. | ||||
|  | ||||
| *DEPRECATED* prefer using explicit numeric suffixes (e.g., `42cm`) or the `to...` conversion functions. | ||||
|  | ||||
| No matter what units the current file uses, this function will always return a number equivalent to the input in centimeters. | ||||
|  | ||||
| For example, if the current file uses inches, `fromCm(1)` will return `0.393701`. If the current file uses millimeters, `fromCm(1)` will return `10`. If the current file uses centimeters, `fromCm(1)` will return `1`. | ||||
|  | ||||
| @ -4,8 +4,12 @@ excerpt: "Converts a number from feet to the current default unit." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| **WARNING:** This function is deprecated. | ||||
|  | ||||
| Converts a number from feet to the current default unit. | ||||
|  | ||||
| *DEPRECATED* prefer using explicit numeric suffixes (e.g., `42ft`) or the `to...` conversion functions. | ||||
|  | ||||
| No matter what units the current file uses, this function will always return a number equivalent to the input in feet. | ||||
|  | ||||
| For example, if the current file uses inches, `fromFt(1)` will return `12`. If the current file uses millimeters, `fromFt(1)` will return `304.8`. If the current file uses feet, `fromFt(1)` will return `1`. | ||||
|  | ||||
| @ -4,8 +4,12 @@ excerpt: "Converts a number from inches to the current default unit." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| **WARNING:** This function is deprecated. | ||||
|  | ||||
| Converts a number from inches to the current default unit. | ||||
|  | ||||
| *DEPRECATED* prefer using explicit numeric suffixes (e.g., `42inch`) or the `to...` conversion functions. | ||||
|  | ||||
| No matter what units the current file uses, this function will always return a number equivalent to the input in inches. | ||||
|  | ||||
| For example, if the current file uses inches, `fromInches(1)` will return `1`. If the current file uses millimeters, `fromInches(1)` will return `25.4`. | ||||
|  | ||||
| @ -4,8 +4,12 @@ excerpt: "Converts a number from meters to the current default unit." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| **WARNING:** This function is deprecated. | ||||
|  | ||||
| Converts a number from meters to the current default unit. | ||||
|  | ||||
| *DEPRECATED* prefer using explicit numeric suffixes (e.g., `42m`) or the `to...` conversion functions. | ||||
|  | ||||
| No matter what units the current file uses, this function will always return a number equivalent to the input in meters. | ||||
|  | ||||
| For example, if the current file uses inches, `fromM(1)` will return `39.3701`. If the current file uses millimeters, `fromM(1)` will return `1000`. If the current file uses meters, `fromM(1)` will return `1`. | ||||
|  | ||||
| @ -4,8 +4,12 @@ excerpt: "Converts a number from mm to the current default unit." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| **WARNING:** This function is deprecated. | ||||
|  | ||||
| Converts a number from mm to the current default unit. | ||||
|  | ||||
| *DEPRECATED* prefer using explicit numeric suffixes (e.g., `42mm`) or the `to...` conversion functions. | ||||
|  | ||||
| No matter what units the current file uses, this function will always return a number equivalent to the input in millimeters. | ||||
|  | ||||
| For example, if the current file uses inches, `fromMm(1)` will return `1/25.4`. If the current file uses millimeters, `fromMm(1)` will return `1`. | ||||
|  | ||||
| @ -4,8 +4,12 @@ excerpt: "Converts a number from yards to the current default unit." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| **WARNING:** This function is deprecated. | ||||
|  | ||||
| Converts a number from yards to the current default unit. | ||||
|  | ||||
| *DEPRECATED* prefer using explicit numeric suffixes (e.g., `42yd`) or the `to...` conversion functions. | ||||
|  | ||||
| No matter what units the current file uses, this function will always return a number equivalent to the input in yards. | ||||
|  | ||||
| For example, if the current file uses inches, `fromYd(1)` will return `36`. If the current file uses millimeters, `fromYd(1)` will return `914.4`. If the current file uses yards, `fromYd(1)` will return `1`. | ||||
|  | ||||
| @ -22,18 +22,8 @@ layout: manual | ||||
|   * [`string`](kcl/types/string) | ||||
|   * [`tag`](kcl/types/tag) | ||||
| * **std** | ||||
|   * [`Axis2d`](kcl/types/Axis2d) | ||||
|   * [`Axis3d`](kcl/types/Axis3d) | ||||
|   * [`END`](kcl/consts/std-END) | ||||
|   * [`Edge`](kcl/types/Edge) | ||||
|   * [`Face`](kcl/types/Face) | ||||
|   * [`Helix`](kcl/types/Helix) | ||||
|   * [`Plane`](kcl/types/Plane) | ||||
|   * [`Point2d`](kcl/types/Point2d) | ||||
|   * [`Point3d`](kcl/types/Point3d) | ||||
|   * [`START`](kcl/consts/std-START) | ||||
|   * [`Sketch`](kcl/types/Sketch) | ||||
|   * [`Solid`](kcl/types/Solid) | ||||
|   * [`X`](kcl/consts/std-X) | ||||
|   * [`XY`](kcl/consts/std-XY) | ||||
|   * [`XZ`](kcl/consts/std-XZ) | ||||
| @ -50,11 +40,7 @@ layout: manual | ||||
|   * [`arc`](kcl/arc) | ||||
|   * [`asin`](kcl/asin) | ||||
|   * [`assert`](kcl/assert) | ||||
|   * [`assertEqual`](kcl/assertEqual) | ||||
|   * [`assertGreaterThan`](kcl/assertGreaterThan) | ||||
|   * [`assertGreaterThanOrEq`](kcl/assertGreaterThanOrEq) | ||||
|   * [`assertLessThan`](kcl/assertLessThan) | ||||
|   * [`assertLessThanOrEq`](kcl/assertLessThanOrEq) | ||||
|   * [`assertIs`](kcl/assertIs) | ||||
|   * [`atan`](kcl/atan) | ||||
|   * [`atan2`](kcl/atan2) | ||||
|   * [`bezierCurve`](kcl/bezierCurve) | ||||
| @ -65,12 +51,6 @@ layout: manual | ||||
|   * [`extrude`](kcl/extrude) | ||||
|   * [`fillet`](kcl/fillet) | ||||
|   * [`floor`](kcl/floor) | ||||
|   * [`fromCm`](kcl/fromCm) | ||||
|   * [`fromFt`](kcl/fromFt) | ||||
|   * [`fromInches`](kcl/fromInches) | ||||
|   * [`fromM`](kcl/fromM) | ||||
|   * [`fromMm`](kcl/fromMm) | ||||
|   * [`fromYd`](kcl/fromYd) | ||||
|   * [`getCommonEdge`](kcl/getCommonEdge) | ||||
|   * [`getNextAdjacentEdge`](kcl/getNextAdjacentEdge) | ||||
|   * [`getOppositeEdge`](kcl/getOppositeEdge) | ||||
| @ -130,8 +110,14 @@ layout: manual | ||||
|   * [`sweep`](kcl/sweep) | ||||
|   * [`tangentToEnd`](kcl/tangentToEnd) | ||||
|   * [`tangentialArc`](kcl/tangentialArc) | ||||
|   * [`toDegrees`](kcl/toDegrees) | ||||
|   * [`toRadians`](kcl/toRadians) | ||||
|   * [`toCentimeters`](kcl/std-toCentimeters) | ||||
|   * [`toDegrees`](kcl/std-toDegrees) | ||||
|   * [`toFeet`](kcl/std-toFeet) | ||||
|   * [`toInches`](kcl/std-toInches) | ||||
|   * [`toMeters`](kcl/std-toMeters) | ||||
|   * [`toMillimeters`](kcl/std-toMillimeters) | ||||
|   * [`toRadians`](kcl/std-toRadians) | ||||
|   * [`toYards`](kcl/std-toYards) | ||||
|   * [`translate`](kcl/translate) | ||||
|   * [`union`](kcl/union) | ||||
|   * [`xLine`](kcl/xLine) | ||||
| @ -152,3 +138,14 @@ layout: manual | ||||
|   * [`turns::QUARTER_TURN`](kcl/consts/std-turns-QUARTER_TURN) | ||||
|   * [`turns::THREE_QUARTER_TURN`](kcl/consts/std-turns-THREE_QUARTER_TURN) | ||||
|   * [`turns::ZERO`](kcl/consts/std-turns-ZERO) | ||||
| * **std::types** | ||||
|   * [`Axis2d`](kcl/types/Axis2d) | ||||
|   * [`Axis3d`](kcl/types/Axis3d) | ||||
|   * [`Edge`](kcl/types/Edge) | ||||
|   * [`Face`](kcl/types/Face) | ||||
|   * [`Helix`](kcl/types/Helix) | ||||
|   * [`Plane`](kcl/types/Plane) | ||||
|   * [`Point2d`](kcl/types/Point2d) | ||||
|   * [`Point3d`](kcl/types/Point3d) | ||||
|   * [`Sketch`](kcl/types/Sketch) | ||||
|   * [`Solid`](kcl/types/Solid) | ||||
|  | ||||
| @ -34,7 +34,7 @@ int(num: number): number | ||||
|  | ||||
| ```js | ||||
| n = int(ceil(5 / 2)) | ||||
| assertEqual(n, 3, 0.0001, "5/2 = 2.5, rounded up makes 3") | ||||
| assert(n, isEqualTo = 3, error = "5/2 = 2.5, rounded up makes 3") | ||||
| // Draw n cylinders. | ||||
| startSketchOn(XZ) | ||||
|   |> circle(center = [0, 0], radius = 2) | ||||
|  | ||||
| @ -21,4 +21,7 @@ once fixed in engine will just start working here with no language changes. | ||||
| - **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple | ||||
|     chamfer cases work currently. | ||||
|  | ||||
| - **Appearance**: Changing the appearance on a loft does not work. | ||||
| - **Appearance**: Changing the appearance on a loft does not work.  | ||||
|     Changing the appearance on an imported model does not work. | ||||
|  | ||||
| - **CSG Booleans**: Coplanar (bodies that share a plane) unions, subtractions, and intersections are not currently supported. | ||||
|  | ||||
| @ -174,7 +174,7 @@ t = 0.005 // taper factor [0-1) | ||||
| // Defines how to modify each layer of the vase. | ||||
| // Each replica is shifted up the Z axis, and has a smoothly-varying radius | ||||
| fn transform(replicaId) { | ||||
|   scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8)) | ||||
|   scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8: number(rad))) | ||||
|   return { | ||||
|     translate = [0, 0, replicaId * 10], | ||||
|     scale = [scale, scale, 0] | ||||
| @ -205,12 +205,12 @@ fn transform(i) { | ||||
| } | ||||
| startSketchOn(XY) | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> polygon({ | ||||
|   |> polygon( | ||||
|        radius = 10, | ||||
|        numSides = 4, | ||||
|        center = [0, 0], | ||||
|        inscribed = false | ||||
|      }, %) | ||||
|        inscribed = false, | ||||
|      ) | ||||
|   |> extrude(length = 4) | ||||
|   |> patternTransform(instances = 3, transform = transform) | ||||
| ``` | ||||
|  | ||||
| @ -10,9 +10,11 @@ Create a regular polygon with the specified number of sides that is either inscr | ||||
|  | ||||
| ```js | ||||
| polygon( | ||||
|   data: PolygonData, | ||||
|   sketchSurfaceOrGroup: SketchOrSurface, | ||||
|   tag?: TagDeclarator, | ||||
|   radius: number, | ||||
|   numSides: u64, | ||||
|   center: [number], | ||||
|   inscribed?: bool, | ||||
| ): Sketch | ||||
| ``` | ||||
|  | ||||
| @ -21,9 +23,11 @@ polygon( | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `data` | [`PolygonData`](/docs/kcl/types/PolygonData) | Data for drawing a polygon | Yes | | ||||
| | `sketchSurfaceOrGroup` | [`SketchOrSurface`](/docs/kcl/types/SketchOrSurface) | A sketch surface or a sketch. | Yes | | ||||
| | [`tag`](/docs/kcl/types/tag) | [`TagDeclarator`](/docs/kcl/types#tag-declaration) |  | No | | ||||
| | `sketchSurfaceOrGroup` | [`SketchOrSurface`](/docs/kcl/types/SketchOrSurface) | Plane or surface to sketch on | Yes | | ||||
| | `radius` | [`number`](/docs/kcl/types/number) | The radius of the polygon | Yes | | ||||
| | `numSides` | `u64` | The number of sides in the polygon | Yes | | ||||
| | `center` | [`[number]`](/docs/kcl/types/number) | The center point of the polygon | Yes | | ||||
| | `inscribed` | [`bool`](/docs/kcl/types/bool) | Whether the polygon is inscribed (true, the default) or circumscribed (false) about a circle with the specified radius | No | | ||||
|  | ||||
| ### Returns | ||||
|  | ||||
| @ -35,12 +39,12 @@ polygon( | ||||
| ```js | ||||
| // Create a regular hexagon inscribed in a circle of radius 10 | ||||
| hex = startSketchOn(XY) | ||||
|   |> polygon({ | ||||
|   |> polygon( | ||||
|        radius = 10, | ||||
|        numSides = 6, | ||||
|        center = [0, 0], | ||||
|        inscribed = true | ||||
|      }, %) | ||||
|        inscribed = true, | ||||
|      ) | ||||
|  | ||||
| example = extrude(hex, length = 5) | ||||
| ``` | ||||
| @ -50,12 +54,12 @@ example = extrude(hex, length = 5) | ||||
| ```js | ||||
| // Create a square circumscribed around a circle of radius 5 | ||||
| square = startSketchOn(XY) | ||||
|   |> polygon({ | ||||
|   |> polygon( | ||||
|        radius = 5.0, | ||||
|        numSides = 4, | ||||
|        center = [10, 10], | ||||
|        inscribed = false | ||||
|      }, %) | ||||
|        inscribed = false, | ||||
|      ) | ||||
| example = extrude(square, length = 5) | ||||
| ``` | ||||
|  | ||||
|  | ||||
| @ -26,7 +26,7 @@ helix( | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `revolutions` | `number(_)` | Number of revolutions. | Yes | | ||||
| | `angleStart` | `number(Angle)` | Start angle (in degrees). | Yes | | ||||
| | `angleStart` | `number(Angle)` | Start angle. | Yes | | ||||
| | `ccw` | [`bool`](/docs/kcl/types/bool) | Is the helix rotation counter clockwise? The default is `false`. | No | | ||||
| | `radius` | `number(Length)` | Radius of the helix. | No | | ||||
| | `axis` | `Axis3d | Edge` | Axis to use for the helix. | No | | ||||
|  | ||||
| @ -31,7 +31,7 @@ exampleSketch = startSketchOn(XZ) | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> angledLine( | ||||
|     angle = 30, | ||||
|     length = 3 / cos(toRadians(30)), | ||||
|     length = 3 / cos(30deg), | ||||
|   ) | ||||
|   |> yLine(endAbsolute = 0) | ||||
|   |> close() | ||||
|  | ||||
| @ -11,7 +11,7 @@ cartesian (x/y/z grid) coordinates. | ||||
|  | ||||
| ```js | ||||
| polar( | ||||
|   angle: number(Angle), | ||||
|   angle: number(rad), | ||||
|   length: number(Length), | ||||
| ): Point2d | ||||
| ``` | ||||
| @ -21,7 +21,7 @@ polar( | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `angle` | `number(Angle)` |  | Yes | | ||||
| | `angle` | `number(rad)` |  | Yes | | ||||
| | `length` | `number(Length)` |  | Yes | | ||||
|  | ||||
| ### Returns | ||||
|  | ||||
| @ -31,7 +31,7 @@ exampleSketch = startSketchOn(XZ) | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> angledLine( | ||||
|     angle = 50, | ||||
|     length = 15 / sin(toRadians(135)), | ||||
|     length = 15 / sin(135deg), | ||||
|   ) | ||||
|   |> yLine(endAbsolute = 0) | ||||
|   |> close() | ||||
|  | ||||
| @ -31,7 +31,7 @@ exampleSketch = startSketchOn(XZ) | ||||
|   |> startProfileAt([0, 0], %) | ||||
|   |> angledLine( | ||||
|     angle = 50, | ||||
|     length = 50 * tan(1/2), | ||||
|     length = 50 * tan((1/2): number(rad)), | ||||
|   ) | ||||
|   |> yLine(endAbsolute = 0) | ||||
|   |> close() | ||||
|  | ||||
							
								
								
									
										27
									
								
								docs/kcl/std-toCentimeters.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,27 @@ | ||||
| --- | ||||
| title: "std::toCentimeters" | ||||
| excerpt: "Convert a number to centimeters from its current units." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Convert a number to centimeters from its current units. | ||||
|  | ||||
|  | ||||
|  | ||||
| ```js | ||||
| toCentimeters(@num: number(cm)): number(cm) | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ### Arguments | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `num` | `number(cm)` |  | Yes | | ||||
|  | ||||
| ### Returns | ||||
|  | ||||
| `number(cm)` | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										44
									
								
								docs/kcl/std-toDegrees.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										27
									
								
								docs/kcl/std-toFeet.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,27 @@ | ||||
| --- | ||||
| title: "std::toFeet" | ||||
| excerpt: "Convert a number to feet from its current units." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Convert a number to feet from its current units. | ||||
|  | ||||
|  | ||||
|  | ||||
| ```js | ||||
| toFeet(@num: number(ft)): number(ft) | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ### Arguments | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `num` | `number(ft)` |  | Yes | | ||||
|  | ||||
| ### Returns | ||||
|  | ||||
| `number(ft)` | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										27
									
								
								docs/kcl/std-toInches.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,27 @@ | ||||
| --- | ||||
| title: "std::toInches" | ||||
| excerpt: "Convert a number to inches from its current units." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Convert a number to inches from its current units. | ||||
|  | ||||
|  | ||||
|  | ||||
| ```js | ||||
| toInches(@num: number(in)): number(in) | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ### Arguments | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `num` | `number(in)` |  | Yes | | ||||
|  | ||||
| ### Returns | ||||
|  | ||||
| `number(in)` | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										27
									
								
								docs/kcl/std-toMeters.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,27 @@ | ||||
| --- | ||||
| title: "std::toMeters" | ||||
| excerpt: "Convert a number to meters from its current units." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Convert a number to meters from its current units. | ||||
|  | ||||
|  | ||||
|  | ||||
| ```js | ||||
| toMeters(@num: number(m)): number(m) | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ### Arguments | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `num` | `number(m)` |  | Yes | | ||||
|  | ||||
| ### Returns | ||||
|  | ||||
| `number(m)` | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										27
									
								
								docs/kcl/std-toMillimeters.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,27 @@ | ||||
| --- | ||||
| title: "std::toMillimeters" | ||||
| excerpt: "Convert a number to millimeters from its current units." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Convert a number to millimeters from its current units. | ||||
|  | ||||
|  | ||||
|  | ||||
| ```js | ||||
| toMillimeters(@num: number(mm)): number(mm) | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ### Arguments | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `num` | `number(mm)` |  | Yes | | ||||
|  | ||||
| ### Returns | ||||
|  | ||||
| `number(mm)` | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										44
									
								
								docs/kcl/std-toRadians.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										27
									
								
								docs/kcl/std-toYards.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,27 @@ | ||||
| --- | ||||
| title: "std::toYards" | ||||
| excerpt: "Converts a number to yards from its current units." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| Converts a number to yards from its current units. | ||||
|  | ||||
|  | ||||
|  | ||||
| ```js | ||||
| toYards(@num: number(yd)): number(yd) | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ### Arguments | ||||
|  | ||||
| | Name | Type | Description | Required | | ||||
| |----------|------|-------------|----------| | ||||
| | `num` | `number(yd)` |  | Yes | | ||||
|  | ||||
| ### Returns | ||||
|  | ||||
| `number(yd)` | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										6765
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						| @ -1,5 +1,5 @@ | ||||
| --- | ||||
| title: "std::Axis2d" | ||||
| title: "std::types::Axis2d" | ||||
| excerpt: "An infinite line in 2d space." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| --- | ||||
| title: "std::Axis3d" | ||||
| title: "std::types::Axis3d" | ||||
| excerpt: "An infinite line in 3d space." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| --- | ||||
| title: "std::Edge" | ||||
| title: "std::types::Edge" | ||||
| excerpt: "The edge of a solid." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| --- | ||||
| title: "std::Face" | ||||
| title: "std::types::Face" | ||||
| excerpt: "A face." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| --- | ||||
| title: "std::Helix" | ||||
| title: "std::types::Helix" | ||||
| excerpt: "A helix." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| --- | ||||
| title: "std::Plane" | ||||
| title: "std::types::Plane" | ||||
| excerpt: "A plane." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| --- | ||||
| title: "std::Point2d" | ||||
| title: "std::types::Point2d" | ||||
| excerpt: "A point in two dimensional space." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| --- | ||||
| title: "std::Point3d" | ||||
| title: "std::types::Point3d" | ||||
| excerpt: "A point in three dimensional space." | ||||
| layout: manual | ||||
| --- | ||||
|  | ||||
| @ -31,7 +31,6 @@ A sketch type. | ||||
| | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's X axis be? | No | | ||||
| | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's Y axis be? | No | | ||||
| | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||
| | `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No | | ||||
|  | ||||
|  | ||||
| ---- | ||||
|  | ||||
| @ -12,7 +12,7 @@ The syntax for declaring a tag is `$myTag` you would use it in the following | ||||
| way: | ||||
|  | ||||
| ```js | ||||
| startSketchOn('XZ') | ||||
| startSketchOn(XZ) | ||||
|   |> startProfileAt(origin, %) | ||||
|   |> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001) | ||||
|   |> angledLine( | ||||
| @ -46,7 +46,7 @@ However if the code was written like this: | ||||
|  | ||||
| ```js | ||||
| fn rect(origin) { | ||||
|   return startSketchOn('XZ') | ||||
|   return startSketchOn(XZ) | ||||
|     |> startProfileAt(origin, %) | ||||
|     |> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001) | ||||
|     |> angledLine( | ||||
| @ -75,7 +75,7 @@ For example the following code works. | ||||
|  | ||||
| ```js | ||||
| fn rect(origin) { | ||||
|   return startSketchOn('XZ') | ||||
|   return startSketchOn(XZ) | ||||
|     |> startProfileAt(origin, %) | ||||
|     |> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001) | ||||
|     |> angledLine( | ||||
|  | ||||
| @ -1,6 +1,25 @@ | ||||
| import type { Reporter, TestCase, TestResult } from '@playwright/test/reporter' | ||||
| import type { | ||||
|   Reporter, | ||||
|   TestCase, | ||||
|   TestResult, | ||||
|   FullResult, | ||||
| } from '@playwright/test/reporter' | ||||
|  | ||||
| class MyAPIReporter implements Reporter { | ||||
|   private pendingRequests: Promise<void>[] = [] | ||||
|   private allResults: Record<string, any>[] = [] | ||||
|   private blockingResults: Record<string, any>[] = [] | ||||
|  | ||||
|   async onEnd(result: FullResult): Promise<void> { | ||||
|     await Promise.all(this.pendingRequests) | ||||
|     if (this.allResults.length > 0 && this.blockingResults.length === 0) { | ||||
|       result.status = 'passed' | ||||
|       if (!process.env.CI) { | ||||
|         console.error('TAB API - Marked failures as non-blocking') | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   onTestEnd(test: TestCase, result: TestResult): void { | ||||
|     if (!process.env.TAB_API_URL || !process.env.TAB_API_KEY) { | ||||
|       return | ||||
| @ -20,6 +39,7 @@ class MyAPIReporter implements Reporter { | ||||
|       platform: process.env.RUNNER_OS || process.platform, | ||||
|       // Extra test and result data | ||||
|       annotations: test.annotations.map((a) => a.type), // e.g. 'fail' or 'fixme' | ||||
|       id: test.id, // computed file/test/project ID used for reruns | ||||
|       retry: result.retry, | ||||
|       tags: test.tags, // e.g. '@snapshot' or '@skipWin' | ||||
|       // Extra environment variables | ||||
| @ -35,7 +55,7 @@ class MyAPIReporter implements Reporter { | ||||
|       RUNNER_ARCH: process.env.RUNNER_ARCH || null, | ||||
|     } | ||||
|  | ||||
|     void (async () => { | ||||
|     const request = (async () => { | ||||
|       try { | ||||
|         const response = await fetch(`${process.env.TAB_API_URL}/api/results`, { | ||||
|           method: 'POST', | ||||
| @ -46,18 +66,27 @@ class MyAPIReporter implements Reporter { | ||||
|           body: JSON.stringify(payload), | ||||
|         }) | ||||
|  | ||||
|         if (!response.ok && !process.env.CI) { | ||||
|           console.error( | ||||
|             'TAB API - Failed to send test result:', | ||||
|             await response.text() | ||||
|           ) | ||||
|         if (response.ok) { | ||||
|           const result = await response.json() | ||||
|           this.allResults.push(result) | ||||
|           if (result.block) { | ||||
|             this.blockingResults.push(result) | ||||
|           } | ||||
|         } else { | ||||
|           const error = await response.json() | ||||
|           if (!process.env.CI) { | ||||
|             console.error('TAB API - Failed to send test result:', error) | ||||
|           } | ||||
|         } | ||||
|       } catch { | ||||
|       } catch (error) { | ||||
|         const message = error instanceof Error ? error.message : String(error) | ||||
|         if (!process.env.CI) { | ||||
|           console.error('TAB API - Unable to send test result') | ||||
|           console.error('TAB API - Unable to send test result:', message) | ||||
|         } | ||||
|       } | ||||
|     })() | ||||
|  | ||||
|     this.pendingRequests.push(request) | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -21,8 +21,9 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { | ||||
|                 if (!app || !app.applicationMenu) { | ||||
|                   return false | ||||
|                 } | ||||
|                 const newProject = | ||||
|                   app.applicationMenu.getMenuItemById('File.New project') | ||||
|                 const newProject = app.applicationMenu.getMenuItemById( | ||||
|                   'File.Create project' | ||||
|                 ) | ||||
|                 if (!newProject) return false | ||||
|                 newProject.click() | ||||
|                 return true | ||||
| @ -484,8 +485,9 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { | ||||
|         await page.waitForTimeout(100) // wait for createModelingPageMenu() to run | ||||
|         await tronApp.electron.evaluate(async ({ app }) => { | ||||
|           if (!app || !app.applicationMenu) fail() | ||||
|           const newProject = | ||||
|             app.applicationMenu.getMenuItemById('File.New project') | ||||
|           const newProject = app.applicationMenu.getMenuItemById( | ||||
|             'File.Create project' | ||||
|           ) | ||||
|           if (!newProject) fail() | ||||
|           newProject.click() | ||||
|         }) | ||||
| @ -608,7 +610,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { | ||||
|         const expected = 'Export' | ||||
|         expect(actual).toBe(expected) | ||||
|       }) | ||||
|       test('Modeling.File.Share current part (via Zoo link)', async ({ | ||||
|       test('Modeling.File.Share part via Zoo link', async ({ | ||||
|         tronApp, | ||||
|         cmdBar, | ||||
|         page, | ||||
| @ -629,10 +631,10 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => { | ||||
|             throw new Error('app or app.applicationMenu is missing') | ||||
|           } | ||||
|           const openProject = app.applicationMenu.getMenuItemById( | ||||
|             'File.Share current part (via Zoo link)' | ||||
|             'File.Share part via Zoo link' | ||||
|           ) | ||||
|           if (!openProject) { | ||||
|             throw new Error('File.Share current part (via Zoo link)') | ||||
|             throw new Error('File.Share part via Zoo link') | ||||
|           } | ||||
|           openProject.click() | ||||
|         }) | ||||
|  | ||||
| @ -63,7 +63,7 @@ test.describe('Onboarding tests', () => { | ||||
|     { | ||||
|       tag: '@electron', | ||||
|     }, | ||||
|     async ({ page, tronApp }) => { | ||||
|     async ({ page, tronApp, scene }) => { | ||||
|       if (!tronApp) { | ||||
|         fail() | ||||
|       } | ||||
| @ -72,7 +72,6 @@ test.describe('Onboarding tests', () => { | ||||
|           onboarding_status: '', | ||||
|         }, | ||||
|       }) | ||||
|       const u = await getUtils(page) | ||||
|  | ||||
|       const viewportSize = { width: 1200, height: 500 } | ||||
|       await page.setBodyDimensions(viewportSize) | ||||
| @ -80,7 +79,7 @@ test.describe('Onboarding tests', () => { | ||||
|       await test.step(`Create a project and open to the onboarding`, async () => { | ||||
|         await createProject({ name: 'project-link', page }) | ||||
|         await test.step(`Ensure the engine connection works by testing the sketch button`, async () => { | ||||
|           await u.waitForPageLoad() | ||||
|           await scene.connectionEstablished() | ||||
|         }) | ||||
|       }) | ||||
|  | ||||
| @ -107,12 +106,10 @@ test.describe('Onboarding tests', () => { | ||||
|   ) | ||||
|  | ||||
|   test('Code resets after confirmation', async ({ | ||||
|     context, | ||||
|     page, | ||||
|     homePage, | ||||
|     tronApp, | ||||
|     scene, | ||||
|     cmdBar, | ||||
|   }) => { | ||||
|     if (!tronApp) { | ||||
|       fail() | ||||
| @ -276,6 +273,8 @@ test.describe('Onboarding tests', () => { | ||||
|     page, | ||||
|     homePage, | ||||
|     tronApp, | ||||
|     editor, | ||||
|     toolbar, | ||||
|   }) => { | ||||
|     if (!tronApp) { | ||||
|       fail() | ||||
| @ -289,7 +288,6 @@ test.describe('Onboarding tests', () => { | ||||
|       }, | ||||
|     }) | ||||
|  | ||||
|     const u = await getUtils(page) | ||||
|     const badCode = `// This is bad code we shouldn't see` | ||||
|  | ||||
|     await page.setBodyDimensions({ width: 1200, height: 1080 }) | ||||
| @ -299,18 +297,19 @@ test.describe('Onboarding tests', () => { | ||||
|       .poll(() => page.url()) | ||||
|       .toContain(onboardingPaths.PARAMETRIC_MODELING) | ||||
|  | ||||
|     const bracketNoNewLines = bracket.replace(/\n/g, '') | ||||
|  | ||||
|     // Check the code got reset on load | ||||
|     await expect(page.locator('#code-pane')).toBeVisible() | ||||
|     await expect(u.codeLocator).toHaveText(bracketNoNewLines, { | ||||
|     await toolbar.openPane('code') | ||||
|     await editor.expectEditor.toContain(bracket, { | ||||
|       shouldNormalise: true, | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|  | ||||
|     // Mess with the code again | ||||
|     await u.codeLocator.selectText() | ||||
|     await u.codeLocator.fill(badCode) | ||||
|     await expect(u.codeLocator).toHaveText(badCode) | ||||
|     await editor.replaceCode('', badCode) | ||||
|     await editor.expectEditor.toContain(badCode, { | ||||
|       shouldNormalise: true, | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|  | ||||
|     // Click to the next step | ||||
|     await page.locator('[data-testid="onboarding-next"]').hover() | ||||
| @ -320,7 +319,10 @@ test.describe('Onboarding tests', () => { | ||||
|     }) | ||||
|  | ||||
|     // Check that the code has been reset | ||||
|     await expect(u.codeLocator).toHaveText(bracketNoNewLines) | ||||
|     await editor.expectEditor.toContain(bracket, { | ||||
|       shouldNormalise: true, | ||||
|       timeout: 10_000, | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   // (lee) The two avatar tests are weird because even on main, we don't have | ||||
|  | ||||
| @ -278,7 +278,7 @@ test.describe('Point-and-click assemblies tests', () => { | ||||
|           highlightedHeaderArg: 'x', | ||||
|           commandName: 'Translate', | ||||
|         }) | ||||
|         await page.keyboard.insertText('5') | ||||
|         await page.keyboard.insertText('100') | ||||
|         await cmdBar.progressCmdBar() | ||||
|         await page.keyboard.insertText('0.1') | ||||
|         await cmdBar.progressCmdBar() | ||||
| @ -287,7 +287,7 @@ test.describe('Point-and-click assemblies tests', () => { | ||||
|         await cmdBar.expectState({ | ||||
|           stage: 'review', | ||||
|           headerArguments: { | ||||
|             X: '5', | ||||
|             X: '100', | ||||
|             Y: '0.1', | ||||
|             Z: '0.2', | ||||
|           }, | ||||
| @ -299,7 +299,7 @@ test.describe('Point-and-click assemblies tests', () => { | ||||
|         await editor.expectEditor.toContain( | ||||
|           ` | ||||
|         bracket | ||||
|           |> translate(x = 5, y = 0.1, z = 0.2) | ||||
|           |> translate(x = 100, y = 0.1, z = 0.2) | ||||
|         `, | ||||
|           { shouldNormalise: true } | ||||
|         ) | ||||
| @ -348,7 +348,7 @@ test.describe('Point-and-click assemblies tests', () => { | ||||
|         await editor.expectEditor.toContain( | ||||
|           ` | ||||
|         bracket | ||||
|           |> translate(x = 5, y = 0.1, z = 0.2) | ||||
|           |> translate(x = 100, y = 0.1, z = 0.2) | ||||
|           |> rotate(roll = 0.1, pitch = 0.2, yaw = 0.3) | ||||
|         `, | ||||
|           { shouldNormalise: true } | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils' | ||||
| import { expect, test } from '@e2e/playwright/zoo-test' | ||||
|  | ||||
| /* eslint-disable jest/no-conditional-expect */ | ||||
| @ -51,7 +50,6 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => { | ||||
|         page, | ||||
|         scene, | ||||
|       }) => { | ||||
|         test.fixme(orRunWhenFullSuiteEnabled()) | ||||
|         await context.addInitScript((file) => { | ||||
|           localStorage.setItem('persistCode', file) | ||||
|         }, file) | ||||
| @ -200,7 +198,6 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => { | ||||
|     page, | ||||
|     scene, | ||||
|   }) => { | ||||
|     test.fixme(orRunWhenFullSuiteEnabled()) | ||||
|     const body1CapCoords = { x: 571, y: 311 } | ||||
|  | ||||
|     await context.addInitScript((file) => { | ||||
| @ -260,7 +257,6 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => { | ||||
|     page, | ||||
|     scene, | ||||
|   }) => { | ||||
|     test.fixme(orRunWhenFullSuiteEnabled()) | ||||
|     const body1CapCoords = { x: 571, y: 311 } | ||||
|     const body2WallCoords = { x: 620, y: 152 } | ||||
|     const [clickBody1Cap] = scene.makeMouseHelpers( | ||||
|  | ||||
| @ -3295,7 +3295,7 @@ profile003 = startProfileAt([-201.08, 254.17], sketch002) | ||||
|         ) | ||||
|         await editor.expectState({ | ||||
|           activeLines: [], | ||||
|           diagnostics: ['memoryitemkey`badBadBadFn`isnotdefined'], | ||||
|           diagnostics: ['`badBadBadFn`isnotdefined'], | ||||
|           highlightedCode: '', | ||||
|         }) | ||||
|         await expect( | ||||
|  | ||||
| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 57 KiB | 
| Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB | 
| Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 69 KiB | 
| Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB | 
| Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB | 
| Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 134 KiB | 
| Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 117 KiB | 
| Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB | 
| Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB | 
| Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB | 
| Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 KiB | 
| Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 68 KiB | 
| Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB | 
| @ -181,7 +181,6 @@ test.describe('Testing settings', () => { | ||||
|   }) | ||||
|  | ||||
|   test('Project and user settings can be reset', async ({ page, homePage }) => { | ||||
|     test.fixme(orRunWhenFullSuiteEnabled()) | ||||
|     const u = await getUtils(page) | ||||
|     await test.step(`Setup`, async () => { | ||||
|       await page.setBodyDimensions({ width: 1200, height: 500 }) | ||||
|  | ||||
| @ -46,12 +46,12 @@ export fn divider(plane) { | ||||
| fn connectorSketch(plane, start) { | ||||
|   sketch001 = startSketchOn(plane) | ||||
|     |> startProfileAt(start, %) | ||||
|     |> polygon({ | ||||
|     |> polygon( | ||||
|          radius = 1.2, | ||||
|          numSides = 6, | ||||
|          center = profileStart(%), | ||||
|          inscribed = false | ||||
|        }, %) | ||||
|          inscribed = false, | ||||
|        ) | ||||
|   return sketch001 | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -25,10 +25,10 @@ shelfMountingHolePlacementOffset = shelfMountingHoleDiameter * 1.5 | ||||
| wallMountingHolePlacementOffset = wallMountingHoleDiameter * 1.5 | ||||
|  | ||||
| // Add checks to ensure bracket is possible. These make sure that there is adequate distance between holes and edges. | ||||
| assertGreaterThanOrEq(wallMountLength, wallMountingHoleDiameter * 3, "Holes not possible. Either decrease hole diameter or increase wallMountLength") | ||||
| assertGreaterThanOrEq(shelfMountLength, shelfMountingHoleDiameter * 5.5, "wallMountLength must be longer for hole sizes to work. Either decrease mounting hole diameters or increase shelfMountLength") | ||||
| assertGreaterThanOrEq(width, shelfMountingHoleDiameter * 5.5, "Holes not possible. Either decrease hole diameter or increase width") | ||||
| assertGreaterThanOrEq(width, wallMountingHoleDiameter * 5.5, "Holes not possible. Either decrease hole diameter or increase width") | ||||
| assert(wallMountLength, isGreaterThanOrEqual = wallMountingHoleDiameter * 3, error = "Holes not possible. Either decrease hole diameter or increase wallMountLength") | ||||
| assert(shelfMountLength, isGreaterThanOrEqual = shelfMountingHoleDiameter * 5.5, error = "wallMountLength must be longer for hole sizes to work. Either decrease mounting hole diameters or increase shelfMountLength") | ||||
| assert(width, isGreaterThanOrEqual = shelfMountingHoleDiameter * 5.5, error = "Holes not possible. Either decrease hole diameter or increase width") | ||||
| assert(width, isGreaterThanOrEqual = wallMountingHoleDiameter * 5.5, error = "Holes not possible. Either decrease hole diameter or increase width") | ||||
|  | ||||
| // Create the body of the bracket | ||||
| bracketBody = startSketchOn(XZ) | ||||
|  | ||||
| @ -38,20 +38,20 @@ plane = { | ||||
|  | ||||
| // Create a regular pentagon inscribed in a circle of radius pentR | ||||
| bottomFace = startSketchOn(XY) | ||||
|   |> polygon({ | ||||
|   |> polygon( | ||||
|        radius = pentR, | ||||
|        numSides = 5, | ||||
|        center = [0, 0], | ||||
|        inscribed = true | ||||
|      }, %) | ||||
|        inscribed = true, | ||||
|      ) | ||||
|  | ||||
| bottomSideFace = startSketchOn(plane) | ||||
|   |> polygon({ | ||||
|   |> polygon( | ||||
|        radius = pentR, | ||||
|        numSides = 5, | ||||
|        center = [0, 0], | ||||
|        inscribed = true | ||||
|      }, %) | ||||
|        inscribed = true, | ||||
|      ) | ||||
|  | ||||
| // Extrude the faces in each plane | ||||
| bottom = extrude(bottomFace, length = wallThickness) | ||||
|  | ||||
| @ -18,7 +18,7 @@ topTotalThickness = totalThickness - (bottomThickness + baseThickness) | ||||
| nHoles = 4 | ||||
|  | ||||
| // Add assertion so nHoles are always greater than 1 | ||||
| assertGreaterThan(nHoles, 1, "nHoles must be greater than 1") | ||||
| assert(nHoles, isGreaterThan = 1, error = "nHoles must be greater than 1") | ||||
|  | ||||
| // Create the circular pattern for the mounting holes | ||||
| circles = startSketchOn(XY) | ||||
|  | ||||
| @ -33,11 +33,11 @@ invas = map(angles, fn(a) { | ||||
|  | ||||
| // Map the involute curve | ||||
| xs = map([0..cmo], fn(i) { | ||||
|   return rs[i] * cos(invas[i]) | ||||
|   return rs[i] * cos(invas[i]: number(rad)) | ||||
| }) | ||||
|  | ||||
| ys = map([0..cmo], fn(i) { | ||||
|   return rs[i] * sin(invas[i]) | ||||
|   return rs[i] * sin(invas[i]: number(rad)) | ||||
| }) | ||||
|  | ||||
| // Extrude the gear body | ||||
|  | ||||
| @ -22,8 +22,8 @@ lSegments = totalLength / lbumps | ||||
| wSegments = totalWidth / wbumps | ||||
|  | ||||
| // Add assertions to ensure that the number of bumps are greater than 1 | ||||
| assertGreaterThan(lbumps, 1, "lbumps must be greater than 1") | ||||
| assertGreaterThan(wbumps, 1, "wbumps must be greater than 1") | ||||
| assert(lbumps, isGreaterThan = 1, error = "lbumps must be greater than 1") | ||||
| assert(wbumps, isGreaterThan = 1, error = "wbumps must be greater than 1") | ||||
|  | ||||
| // Make the base | ||||
| base = startSketchOn(XY) | ||||
|  | ||||
| @ -29,7 +29,7 @@ sketch001 = startSketchOn(XZ) | ||||
|      ) | ||||
|   |> yLine(endAbsolute = -templateThickness, tag = $seg03) | ||||
|   |> xLine(length = templateThickness, tag = $seg07) | ||||
|   |> yLine(endAbsolute = (segEndY(seg01) + templateThickness) / 2 - templateThickness, tag = $seg02) | ||||
|   |> yLine(endAbsolute = (segEndY(seg01) + templateThickness) / 2_ - templateThickness, tag = $seg02) | ||||
|   |> xLine(endAbsolute = segEndX(seg03) + minClampingDistance, tag = $seg06) | ||||
|   |> yLine(length = templateThickness * 2, tag = $seg08) | ||||
|   |> xLine(endAbsolute = segEndX(seg02) + 0, tag = $seg05) | ||||
|  | ||||
| @ -4,7 +4,7 @@ const arr = [0, 0, 0, 10] | ||||
| const i = 3 | ||||
| const ten = arr[i] | ||||
|  | ||||
| assertEqual(ten, 10, 0.000001, "oops") | ||||
| assert(ten, isEqualTo = 10, error = "oops") | ||||
|  | ||||
| const p = "foo" | ||||
| const obj = { | ||||
| @ -13,4 +13,4 @@ const obj = { | ||||
| } | ||||
| const one = obj[p] | ||||
|  | ||||
| assertEqual(one, 1, 0.0000001, "oops") | ||||
| assert(one, isEqualTo = 1, error = "oops") | ||||
|  | ||||
| @ -1,25 +1,25 @@ | ||||
| import identity from "identity.kcl" | ||||
|  | ||||
| answer = identity(42) | ||||
| assertEqual(answer, 42, 0.0001, "identity") | ||||
| assert(answer, isEqualTo = 42, error = "identity") | ||||
|  | ||||
| import identity as id from "identity.kcl" | ||||
|  | ||||
| answer43 = id(43) | ||||
| assertEqual(answer43, 43, 0.0001, "identity") | ||||
| assert(answer43, isEqualTo = 43, error = "identity") | ||||
|  | ||||
| import increment, decrement from "numbers.kcl" | ||||
|  | ||||
| answer3 = increment(2) | ||||
| assertEqual(answer3, 3, 0.0001, "increment") | ||||
| assert(answer3, isEqualTo = 3, error = "increment") | ||||
|  | ||||
| answer5 = decrement(6) | ||||
| assertEqual(answer5, 5, 0.0001, "decrement") | ||||
| assert(answer5, isEqualTo = 5, error = "decrement") | ||||
|  | ||||
| import increment as inc, decrement as dec from "numbers.kcl" | ||||
|  | ||||
| answer4 = inc(3) | ||||
| assertEqual(answer4, 4, 0.0001, "inc") | ||||
| assert(answer4, isEqualTo = 4, error = "inc") | ||||
|  | ||||
| answer6 = dec(7) | ||||
| assertEqual(answer6, 6, 0.0001, "dec") | ||||
| assert(answer6, isEqualTo = 6, error = "dec") | ||||
|  | ||||
| @ -5,7 +5,7 @@ const t = 0.005 // taper factor [0-1) | ||||
| // Defines how to modify each layer of the vase. | ||||
| // Each replica is shifted up the Z axis, and has a smoothly-varying radius | ||||
| fn transform = (replicaId) => { | ||||
|   let scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8)) | ||||
|   let scale = r * abs(1 - (t * replicaId)) * (5 + cos((replicaId / 8): number(rad))) | ||||
|   return { | ||||
|     translate: [0, 0, replicaId * 10], | ||||
|     scale: [scale, scale, 0], | ||||
|  | ||||
| @ -1,52 +0,0 @@ | ||||
| wall_thickness = 0.125 | ||||
| back_walls_width = 2 | ||||
| front_walls_width = 2.5 | ||||
| height = 5.5 | ||||
| filletRadius = 0.050 | ||||
| back_length = 7 | ||||
| exit_height = 1 | ||||
| front_length = 6 | ||||
| Fx = 0.5 | ||||
| Fy = 0.5 | ||||
|  | ||||
| sketch001 = startSketchOn('-YZ') | ||||
|   |> startProfileAt([back_walls_width / 2, 0], %) | ||||
|   |> xLine(length = wall_thickness / 2) | ||||
|   |> angledLine(angle = 45, endAbsoluteX = back_walls_width, tag = $seg01) | ||||
|   |> yLine(endAbsolute = height) | ||||
|   |> xLine(length = -wall_thickness) | ||||
|   |> yLine(endAbsolute = segEndY(seg01)) | ||||
|   |> angledLine(angle = 45, endAbsoluteX = back_walls_width / 2 + wall_thickness / 2) | ||||
|   |> xLine(length = -wall_thickness) | ||||
|   |> angledLine(angle = 180 - 45, endAbsoluteX = wall_thickness) | ||||
|   |> yLine(endAbsolute = height) | ||||
|   |> xLine(endAbsolute = 0) | ||||
|   |> yLine(endAbsolute = segEndY(seg01)) | ||||
|   |> angledLine(angle = 180 - 45, endAbsoluteY = 0) | ||||
|   |> close() | ||||
| part001 = revolve({ | ||||
|   angle: 90, | ||||
|   axis: { | ||||
|     custom: { | ||||
|       axis: [1.0, 0.0], | ||||
|       origin: [0.0, height + .0000001] | ||||
|     } | ||||
|   } | ||||
| }, sketch001) | ||||
|  | ||||
| sketch002 = startSketchOn('-YZ') | ||||
|   |> startProfileAt([back_walls_width / 2, 0], %) | ||||
|   |> xLine(length = wall_thickness / 2) | ||||
|   |> angledLine(angle = 45, endAbsoluteX = back_walls_width, tag = $seg02) | ||||
|   |> yLine(endAbsolute = height) | ||||
|   |> xLine(length = -wall_thickness) | ||||
|   |> yLine(endAbsolute = segEndY(seg01)) | ||||
|   |> angledLine(angle = 45, endAbsoluteX = back_walls_width / 2 + wall_thickness / 2) | ||||
|   |> xLine(length = -wall_thickness) | ||||
|   |> angledLine(angle = 180 - 45, endAbsoluteX = wall_thickness) | ||||
|   |> yLine(endAbsolute = height) | ||||
|   |> xLine(endAbsolute = 0) | ||||
|   |> yLine(endAbsolute = segEndY(seg02)) | ||||
|   |> angledLine(angle = 180 - 45, endAbsoluteY = 0) | ||||
|   |> close() | ||||
|   |> extrude(length = back_length - height) | ||||
| @ -180,10 +180,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|         // Reset to the default units.  Modules assume the engine starts in the | ||||
|         // default state. | ||||
|         self.set_units(Default::default(), source_range, id_generator).await?; | ||||
|  | ||||
|         // Flush the batch queue, so clear is run right away. | ||||
|         // Otherwise the hooks below won't work. | ||||
|         self.flush_batch(false, source_range).await?; | ||||
| @ -298,23 +294,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     async fn set_units( | ||||
|         &self, | ||||
|         units: crate::UnitLength, | ||||
|         source_range: SourceRange, | ||||
|         id_generator: &mut IdGenerator, | ||||
|     ) -> Result<(), crate::errors::KclError> { | ||||
|         // Before we even start executing the program, set the units. | ||||
|         self.batch_modeling_cmd( | ||||
|             id_generator.next_uuid(), | ||||
|             source_range, | ||||
|             &ModelingCmd::from(mcmd::SetSceneUnits { unit: units.into() }), | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Re-run the command to apply the settings. | ||||
|     async fn reapply_settings( | ||||
|         &self, | ||||
|  | ||||
| @ -3,11 +3,7 @@ use std::collections::HashMap; | ||||
| use async_recursion::async_recursion; | ||||
| use indexmap::IndexMap; | ||||
|  | ||||
| use super::{ | ||||
|     cad_op::Group, | ||||
|     kcl_value::TypeDef, | ||||
|     types::{PrimitiveType, CHECK_NUMERIC_TYPES}, | ||||
| }; | ||||
| use super::{cad_op::Group, kcl_value::TypeDef, types::PrimitiveType}; | ||||
| use crate::{ | ||||
|     errors::{KclError, KclErrorDetails}, | ||||
|     execution::{ | ||||
| @ -64,14 +60,6 @@ impl ExecutorContext { | ||||
|                     if exec_state.mod_local.settings.update_from_annotation(annotation)? { | ||||
|                         exec_state.mod_local.explicit_length_units = true; | ||||
|                     } | ||||
|                     let new_units = exec_state.length_unit(); | ||||
|                     self.engine | ||||
|                         .set_units( | ||||
|                             new_units.into(), | ||||
|                             annotation.as_source_range(), | ||||
|                             exec_state.id_generator(), | ||||
|                         ) | ||||
|                         .await?; | ||||
|                 } else { | ||||
|                     exec_state.err(CompilationError::err( | ||||
|                         annotation.as_source_range(), | ||||
| @ -170,20 +158,23 @@ impl ExecutorContext { | ||||
|                                 self.exec_module_for_items(module_id, exec_state, source_range).await?; | ||||
|                             for import_item in items { | ||||
|                                 // Extract the item from the module. | ||||
|                                 let item = exec_state | ||||
|                                     .stack() | ||||
|                                     .memory | ||||
|                                 let mem = &exec_state.stack().memory; | ||||
|                                 let mut value = mem | ||||
|                                     .get_from(&import_item.name.name, env_ref, import_item.into(), 0) | ||||
|                                     .map_err(|_err| { | ||||
|                                         KclError::UndefinedValue(KclErrorDetails { | ||||
|                                             message: format!("{} is not defined in module", import_item.name.name), | ||||
|                                             source_ranges: vec![SourceRange::from(&import_item.name)], | ||||
|                                         }) | ||||
|                                     })? | ||||
|                                     .clone(); | ||||
|                                 // Check that the item is allowed to be imported. | ||||
|                                 if !module_exports.contains(&import_item.name.name) { | ||||
|                                     return Err(KclError::Semantic(KclErrorDetails { | ||||
|                                     .cloned(); | ||||
|                                 let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.name.name); | ||||
|                                 let mut ty = mem.get_from(&ty_name, env_ref, import_item.into(), 0).cloned(); | ||||
|  | ||||
|                                 if value.is_err() && ty.is_err() { | ||||
|                                     return Err(KclError::UndefinedValue(KclErrorDetails { | ||||
|                                         message: format!("{} is not defined in module", import_item.name.name), | ||||
|                                         source_ranges: vec![SourceRange::from(&import_item.name)], | ||||
|                                     })); | ||||
|                                 } | ||||
|  | ||||
|                                 // Check that the item is allowed to be imported (in at least one namespace). | ||||
|                                 if value.is_ok() && !module_exports.contains(&import_item.name.name) { | ||||
|                                     value = Err(KclError::Semantic(KclErrorDetails { | ||||
|                                         message: format!( | ||||
|                                             "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.", | ||||
|                                             import_item.name.name | ||||
| @ -192,18 +183,45 @@ impl ExecutorContext { | ||||
|                                     })); | ||||
|                                 } | ||||
|  | ||||
|                                 // Add the item to the current module. | ||||
|                                 exec_state.mut_stack().add( | ||||
|                                     import_item.identifier().to_owned(), | ||||
|                                     item, | ||||
|                                     SourceRange::from(&import_item.name), | ||||
|                                 )?; | ||||
|                                 if ty.is_ok() && !module_exports.contains(&ty_name) { | ||||
|                                     ty = Err(KclError::Semantic(KclErrorDetails { | ||||
|                                         message: String::new(), | ||||
|                                         source_ranges: vec![], | ||||
|                                     })); | ||||
|                                 } | ||||
|  | ||||
|                                 if let ItemVisibility::Export = import_stmt.visibility { | ||||
|                                     exec_state | ||||
|                                         .mod_local | ||||
|                                         .module_exports | ||||
|                                         .push(import_item.identifier().to_owned()); | ||||
|                                 if value.is_err() && ty.is_err() { | ||||
|                                     return value.map(Option::Some); | ||||
|                                 } | ||||
|  | ||||
|                                 // Add the item to the current module. | ||||
|                                 if let Ok(value) = value { | ||||
|                                     exec_state.mut_stack().add( | ||||
|                                         import_item.identifier().to_owned(), | ||||
|                                         value, | ||||
|                                         SourceRange::from(&import_item.name), | ||||
|                                     )?; | ||||
|  | ||||
|                                     if let ItemVisibility::Export = import_stmt.visibility { | ||||
|                                         exec_state | ||||
|                                             .mod_local | ||||
|                                             .module_exports | ||||
|                                             .push(import_item.identifier().to_owned()); | ||||
|                                     } | ||||
|                                 } | ||||
|  | ||||
|                                 if let Ok(ty) = ty { | ||||
|                                     let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.identifier()); | ||||
|                                     // Add the item to the current module. | ||||
|                                     exec_state.mut_stack().add( | ||||
|                                         ty_name.clone(), | ||||
|                                         ty, | ||||
|                                         SourceRange::from(&import_item.name), | ||||
|                                     )?; | ||||
|  | ||||
|                                     if let ItemVisibility::Export = import_stmt.visibility { | ||||
|                                         exec_state.mod_local.module_exports.push(ty_name); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
| @ -298,19 +316,20 @@ impl ExecutorContext { | ||||
|                                 value: TypeDef::RustRepr(t, props), | ||||
|                                 meta: vec![metadata], | ||||
|                             }; | ||||
|                             let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name); | ||||
|                             exec_state | ||||
|                                 .mut_stack() | ||||
|                                 .add( | ||||
|                                     format!("{}{}", memory::TYPE_PREFIX, ty.name.name), | ||||
|                                     value, | ||||
|                                     metadata.source_range, | ||||
|                                 ) | ||||
|                                 .add(name_in_mem.clone(), value, metadata.source_range) | ||||
|                                 .map_err(|_| { | ||||
|                                     KclError::Semantic(KclErrorDetails { | ||||
|                                         message: format!("Redefinition of type {}.", ty.name.name), | ||||
|                                         source_ranges: vec![metadata.source_range], | ||||
|                                     }) | ||||
|                                 })?; | ||||
|  | ||||
|                             if let ItemVisibility::Export = ty.visibility { | ||||
|                                 exec_state.mod_local.module_exports.push(name_in_mem); | ||||
|                             } | ||||
|                         } | ||||
|                         // Do nothing for primitive types, they get special treatment and their declarations are just for documentation. | ||||
|                         annotations::Impl::Primitive => {} | ||||
| @ -327,19 +346,20 @@ impl ExecutorContext { | ||||
|                                     ), | ||||
|                                     meta: vec![metadata], | ||||
|                                 }; | ||||
|                                 let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name); | ||||
|                                 exec_state | ||||
|                                     .mut_stack() | ||||
|                                     .add( | ||||
|                                         format!("{}{}", memory::TYPE_PREFIX, ty.name.name), | ||||
|                                         value, | ||||
|                                         metadata.source_range, | ||||
|                                     ) | ||||
|                                     .add(name_in_mem.clone(), value, metadata.source_range) | ||||
|                                     .map_err(|_| { | ||||
|                                         KclError::Semantic(KclErrorDetails { | ||||
|                                             message: format!("Redefinition of type {}.", ty.name.name), | ||||
|                                             source_ranges: vec![metadata.source_range], | ||||
|                                         }) | ||||
|                                     })?; | ||||
|  | ||||
|                                 if let ItemVisibility::Export = ty.visibility { | ||||
|                                     exec_state.mod_local.module_exports.push(name_in_mem); | ||||
|                                 } | ||||
|                             } | ||||
|                             None => { | ||||
|                                 return Err(KclError::Semantic(KclErrorDetails { | ||||
| @ -485,7 +505,7 @@ impl ExecutorContext { | ||||
|                 message: "Cannot import items from foreign modules".to_owned(), | ||||
|                 source_ranges: vec![geom.source_range], | ||||
|             })), | ||||
|             ModuleRepr::Dummy => unreachable!(), | ||||
|             ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path), | ||||
|         }; | ||||
|  | ||||
|         exec_state.global.module_infos[&module_id].restore_repr(repr); | ||||
| @ -559,10 +579,10 @@ impl ExecutorContext { | ||||
|                 // It was an import cycle.  Keep the original message. | ||||
|                 err.override_source_ranges(vec![source_range]) | ||||
|             } else { | ||||
|                 // TODO would be great to have line/column for the underlying error here | ||||
|                 KclError::Semantic(KclErrorDetails { | ||||
|                     message: format!( | ||||
|                         "Error loading imported file. Open it to view more details. {}: {}", | ||||
|                         path, | ||||
|                         "Error loading imported file ({path}). Open it to view more details.\n  {}", | ||||
|                         err.message() | ||||
|                     ), | ||||
|                     source_ranges: vec![source_range], | ||||
| @ -841,7 +861,10 @@ impl Node<MemberExpression> { | ||||
|                     source_ranges: vec![self.clone().into()], | ||||
|                 })) | ||||
|             } | ||||
|             (KclValue::MixedArray { value: arr, meta: _ }, Property::UInt(index)) => { | ||||
|             ( | ||||
|                 KclValue::MixedArray { value: arr, .. } | KclValue::HomArray { value: arr, .. }, | ||||
|                 Property::UInt(index), | ||||
|             ) => { | ||||
|                 let value_of_arr = arr.get(index); | ||||
|                 if let Some(value) = value_of_arr { | ||||
|                     Ok(value.to_owned()) | ||||
| @ -852,7 +875,7 @@ impl Node<MemberExpression> { | ||||
|                     })) | ||||
|                 } | ||||
|             } | ||||
|             (KclValue::MixedArray { .. }, p) => { | ||||
|             (KclValue::MixedArray { .. } | KclValue::HomArray { .. }, p) => { | ||||
|                 let t = p.type_name(); | ||||
|                 let article = article_for(t); | ||||
|                 Err(KclError::Semantic(KclErrorDetails { | ||||
| @ -1019,7 +1042,7 @@ impl Node<BinaryExpression> { | ||||
|             BinaryOperator::Pow => KclValue::Number { | ||||
|                 value: left.n.powf(right.n), | ||||
|                 meta, | ||||
|                 ty: NumericType::Unknown, | ||||
|                 ty: exec_state.current_default_units(), | ||||
|             }, | ||||
|             BinaryOperator::Neq => { | ||||
|                 let (l, r, ty) = NumericType::combine_eq(left, right); | ||||
| @ -1058,7 +1081,7 @@ impl Node<BinaryExpression> { | ||||
|     } | ||||
|  | ||||
|     fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) { | ||||
|         if *CHECK_NUMERIC_TYPES && ty == &NumericType::Unknown { | ||||
|         if ty == &NumericType::Unknown { | ||||
|             // TODO suggest how to fix this | ||||
|             exec_state.warn(CompilationError::err( | ||||
|                 self.as_source_range(), | ||||
| @ -1967,11 +1990,39 @@ fn assign_args_to_params( | ||||
|     for (index, param) in function_expression.params.iter().enumerate() { | ||||
|         if let Some(arg) = args.get(index) { | ||||
|             // Argument was provided. | ||||
|             exec_state.mut_stack().add( | ||||
|                 param.identifier.name.clone(), | ||||
|                 arg.value.clone(), | ||||
|                 (¶m.identifier).into(), | ||||
|             )?; | ||||
|  | ||||
|             if let Some(ty) = ¶m.type_ { | ||||
|                 let value = arg | ||||
|                         .value | ||||
|                         .coerce( | ||||
|                             &RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.source_range).unwrap(), | ||||
|                             exec_state, | ||||
|                         ) | ||||
|                         .map_err(|e| { | ||||
|                             let mut message = format!( | ||||
|                                 "Argument requires a value with type `{}`, but found {}", | ||||
|                                 ty.inner, | ||||
|                                 arg.value.human_friendly_type(), | ||||
|                             ); | ||||
|                             if let Some(ty) = e.explicit_coercion { | ||||
|                                 // TODO if we have access to the AST for the argument we could choose which example to suggest. | ||||
|                                 message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n  using a numeric suffix: `42{ty}`\n  or using type ascription: `foo(): number({ty})`"); | ||||
|                             } | ||||
|                             KclError::Semantic(KclErrorDetails { | ||||
|                                 message, | ||||
|                                 source_ranges: vec![arg.source_range], | ||||
|                             }) | ||||
|                         })?; | ||||
|                 exec_state | ||||
|                     .mut_stack() | ||||
|                     .add(param.identifier.name.clone(), value, (¶m.identifier).into())?; | ||||
|             } else { | ||||
|                 exec_state.mut_stack().add( | ||||
|                     param.identifier.name.clone(), | ||||
|                     arg.value.clone(), | ||||
|                     (¶m.identifier).into(), | ||||
|                 )?; | ||||
|             } | ||||
|         } else { | ||||
|             // Argument was not provided. | ||||
|             if let Some(ref default_val) = param.default_value { | ||||
| @ -2139,6 +2190,34 @@ fn assign_args_to_params_kw( | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn coerce_result_type( | ||||
|     result: Result<Option<KclValue>, KclError>, | ||||
|     function_expression: NodeRef<'_, FunctionExpression>, | ||||
|     exec_state: &mut ExecState, | ||||
| ) -> Result<Option<KclValue>, KclError> { | ||||
|     if let Ok(Some(val)) = result { | ||||
|         if let Some(ret_ty) = &function_expression.return_type { | ||||
|             let ty = RuntimeType::from_parsed(ret_ty.inner.clone(), exec_state, ret_ty.as_source_range()) | ||||
|                 .map_err(|e| KclError::Semantic(e.into()))?; | ||||
|             let val = val.coerce(&ty, exec_state).map_err(|_| { | ||||
|                 KclError::Semantic(KclErrorDetails { | ||||
|                     message: format!( | ||||
|                         "This function requires its result to be of type `{}`, but found {}", | ||||
|                         ty.human_friendly_type(), | ||||
|                         val.human_friendly_type(), | ||||
|                     ), | ||||
|                     source_ranges: ret_ty.as_source_ranges(), | ||||
|                 }) | ||||
|             })?; | ||||
|             Ok(Some(val)) | ||||
|         } else { | ||||
|             Ok(Some(val)) | ||||
|         } | ||||
|     } else { | ||||
|         result | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn call_user_defined_function( | ||||
|     args: Vec<Arg>, | ||||
|     memory: EnvironmentRef, | ||||
| @ -2159,13 +2238,16 @@ async fn call_user_defined_function( | ||||
|     let result = ctx | ||||
|         .exec_block(&function_expression.body, exec_state, BodyType::Block) | ||||
|         .await; | ||||
|     let result = result.map(|_| { | ||||
|     let mut result = result.map(|_| { | ||||
|         exec_state | ||||
|             .stack() | ||||
|             .get(memory::RETURN_NAME, function_expression.as_source_range()) | ||||
|             .ok() | ||||
|             .cloned() | ||||
|     }); | ||||
|  | ||||
|     result = coerce_result_type(result, function_expression, exec_state); | ||||
|  | ||||
|     // Restore the previous memory. | ||||
|     exec_state.mut_stack().pop_env(); | ||||
|  | ||||
| @ -2193,13 +2275,16 @@ async fn call_user_defined_function_kw( | ||||
|     let result = ctx | ||||
|         .exec_block(&function_expression.body, exec_state, BodyType::Block) | ||||
|         .await; | ||||
|     let result = result.map(|_| { | ||||
|     let mut result = result.map(|_| { | ||||
|         exec_state | ||||
|             .stack() | ||||
|             .get(memory::RETURN_NAME, function_expression.as_source_range()) | ||||
|             .ok() | ||||
|             .cloned() | ||||
|     }); | ||||
|  | ||||
|     result = coerce_result_type(result, function_expression, exec_state); | ||||
|  | ||||
|     // Restore the previous memory. | ||||
|     exec_state.mut_stack().pop_env(); | ||||
|  | ||||
| @ -2695,6 +2780,27 @@ foo(x = { direction = [0, 0], origin = [0, 0]}) | ||||
| } | ||||
|  | ||||
| foo(x = { direction = [0, 0], origin = [0, 0]}) | ||||
| "#; | ||||
|  | ||||
|         parse_execute(program).await.unwrap_err(); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn coerce_return() { | ||||
|         let program = r#"fn foo(): number(mm) { | ||||
|   return 42 | ||||
| } | ||||
|  | ||||
| a = foo() | ||||
| "#; | ||||
|  | ||||
|         parse_execute(program).await.unwrap(); | ||||
|  | ||||
|         let program = r#"fn foo(): number(mm) { | ||||
|   return { bar: 42 } | ||||
| } | ||||
|  | ||||
| a = foo() | ||||
| "#; | ||||
|  | ||||
|         parse_execute(program).await.unwrap_err(); | ||||
|  | ||||
| @ -10,14 +10,13 @@ use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::{ | ||||
|     errors::KclError, | ||||
|     execution::{types::NumericType, ArtifactId, ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen}, | ||||
|     execution::{ | ||||
|         types::NumericType, ArtifactId, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen, | ||||
|     }, | ||||
|     parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode}, | ||||
|     std::{args::TyF64, sketch::PlaneData}, | ||||
| }; | ||||
|  | ||||
| use super::ExecutorContext; | ||||
|  | ||||
| type Point2D = kcmc::shared::Point2d<f64>; | ||||
| type Point3D = kcmc::shared::Point3d<f64>; | ||||
|  | ||||
| /// A geometry. | ||||
| @ -265,7 +264,6 @@ pub struct Plane { | ||||
|     pub y_axis: Point3d, | ||||
|     /// The z-axis (normal). | ||||
|     pub z_axis: Point3d, | ||||
|     pub units: UnitLen, | ||||
|     #[serde(skip)] | ||||
|     pub meta: Vec<Metadata>, | ||||
| } | ||||
| @ -287,6 +285,8 @@ impl Plane { | ||||
|                             x: 1.0, | ||||
|                             y: 0.0, | ||||
|                             z: 0.0, | ||||
|                             // TODO axes must be normalized, so maybe these should all be count | ||||
|                             // rather than mm? | ||||
|                             units: UnitLen::Mm, | ||||
|                         }, | ||||
|                     y_axis: | ||||
| @ -483,7 +483,6 @@ impl Plane { | ||||
|                 y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm), | ||||
|                 z_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm), | ||||
|                 value: PlaneType::XY, | ||||
|                 units: exec_state.length_unit(), | ||||
|                 meta: vec![], | ||||
|             }, | ||||
|             PlaneData::NegXY => Plane { | ||||
| @ -494,7 +493,6 @@ impl Plane { | ||||
|                 y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm), | ||||
|                 z_axis: Point3d::new(0.0, 0.0, -1.0, UnitLen::Mm), | ||||
|                 value: PlaneType::XY, | ||||
|                 units: exec_state.length_unit(), | ||||
|                 meta: vec![], | ||||
|             }, | ||||
|             PlaneData::XZ => Plane { | ||||
| @ -505,7 +503,6 @@ impl Plane { | ||||
|                 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm), | ||||
|                 z_axis: Point3d::new(0.0, -1.0, 0.0, UnitLen::Mm), | ||||
|                 value: PlaneType::XZ, | ||||
|                 units: exec_state.length_unit(), | ||||
|                 meta: vec![], | ||||
|             }, | ||||
|             PlaneData::NegXZ => Plane { | ||||
| @ -516,7 +513,6 @@ impl Plane { | ||||
|                 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm), | ||||
|                 z_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm), | ||||
|                 value: PlaneType::XZ, | ||||
|                 units: exec_state.length_unit(), | ||||
|                 meta: vec![], | ||||
|             }, | ||||
|             PlaneData::YZ => Plane { | ||||
| @ -527,7 +523,6 @@ impl Plane { | ||||
|                 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm), | ||||
|                 z_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Mm), | ||||
|                 value: PlaneType::YZ, | ||||
|                 units: exec_state.length_unit(), | ||||
|                 meta: vec![], | ||||
|             }, | ||||
|             PlaneData::NegYZ => Plane { | ||||
| @ -538,7 +533,6 @@ impl Plane { | ||||
|                 y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm), | ||||
|                 z_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Mm), | ||||
|                 value: PlaneType::YZ, | ||||
|                 units: exec_state.length_unit(), | ||||
|                 meta: vec![], | ||||
|             }, | ||||
|             PlaneData::Plane { | ||||
| @ -556,7 +550,6 @@ impl Plane { | ||||
|                     y_axis, | ||||
|                     z_axis, | ||||
|                     value: PlaneType::Custom, | ||||
|                     units: exec_state.length_unit(), | ||||
|                     meta: vec![], | ||||
|                 } | ||||
|             } | ||||
| @ -713,12 +706,6 @@ impl SketchSurface { | ||||
|             SketchSurface::Face(face) => face.z_axis, | ||||
|         } | ||||
|     } | ||||
|     pub(crate) fn units(&self) -> UnitLen { | ||||
|         match self { | ||||
|             SketchSurface::Plane(plane) => plane.units, | ||||
|             SketchSurface::Face(face) => face.units, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| @ -787,7 +774,8 @@ impl Sketch { | ||||
|             return Ok(Point2d::new(self.start.to[0], self.start.to[1], self.start.units)); | ||||
|         }; | ||||
|  | ||||
|         Ok(path.get_to().into()) | ||||
|         let to = path.get_base().to; | ||||
|         Ok(Point2d::new(to[0], to[1], path.get_base().units)) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn get_tangential_info_from_paths(&self) -> GetTangentialInfoFromPathsResult { | ||||
| @ -829,6 +817,10 @@ impl Solid { | ||||
|     pub(crate) fn get_all_edge_cut_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ { | ||||
|         self.edge_cuts.iter().map(|foc| foc.id()) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn height_in_mm(&self) -> f64 { | ||||
|         self.units.adjust_to(self.height, UnitLen::Mm).0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A fillet or a chamfer. | ||||
| @ -889,28 +881,6 @@ pub struct Point2d { | ||||
|     pub units: UnitLen, | ||||
| } | ||||
|  | ||||
| impl From<[TyF64; 2]> for Point2d { | ||||
|     fn from(p: [TyF64; 2]) -> Self { | ||||
|         Self { | ||||
|             x: p[0].n, | ||||
|             y: p[1].n, | ||||
|             units: p[0].ty.expect_length(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<Point2d> for [f64; 2] { | ||||
|     fn from(p: Point2d) -> Self { | ||||
|         [p.x, p.y] | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<Point2d> for Point2D { | ||||
|     fn from(p: Point2d) -> Self { | ||||
|         Self { x: p.x, y: p.y } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Point2d { | ||||
|     pub const ZERO: Self = Self { | ||||
|         x: 0.0, | ||||
| @ -921,6 +891,18 @@ impl Point2d { | ||||
|     pub fn new(x: f64, y: f64, units: UnitLen) -> Self { | ||||
|         Self { x, y, units } | ||||
|     } | ||||
|  | ||||
|     pub fn into_x(self) -> TyF64 { | ||||
|         TyF64::new(self.x, self.units.into()) | ||||
|     } | ||||
|  | ||||
|     pub fn into_y(self) -> TyF64 { | ||||
|         TyF64::new(self.y, self.units.into()) | ||||
|     } | ||||
|  | ||||
|     pub fn ignore_units(self) -> [f64; 2] { | ||||
|         [self.x, self.y] | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)] | ||||
| @ -968,9 +950,9 @@ impl From<Point3d> for Point3D { | ||||
| impl From<Point3d> for kittycad_modeling_cmds::shared::Point3d<LengthUnit> { | ||||
|     fn from(p: Point3d) -> Self { | ||||
|         Self { | ||||
|             x: LengthUnit(p.x), | ||||
|             y: LengthUnit(p.y), | ||||
|             z: LengthUnit(p.z), | ||||
|             x: LengthUnit(p.units.adjust_to(p.x, UnitLen::Mm).0), | ||||
|             y: LengthUnit(p.units.adjust_to(p.y, UnitLen::Mm).0), | ||||
|             z: LengthUnit(p.units.adjust_to(p.z, UnitLen::Mm).0), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1318,9 +1300,9 @@ impl Path { | ||||
|                 ccw: *ccw, | ||||
|             }, | ||||
|             Path::ArcThreePoint { p1, p2, p3, .. } => { | ||||
|                 let circle_center = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]); | ||||
|                 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]); | ||||
|                 GetTangentialInfoFromPathsResult::Arc { | ||||
|                     center: circle_center.center, | ||||
|                     center: circle.center, | ||||
|                     ccw: crate::std::utils::is_points_ccw(&[*p1, *p2, *p3]) > 0, | ||||
|                 } | ||||
|             } | ||||
| @ -1332,14 +1314,13 @@ impl Path { | ||||
|                 radius: *radius, | ||||
|             }, | ||||
|             Path::CircleThreePoint { p1, p2, p3, .. } => { | ||||
|                 let circle_center = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]); | ||||
|                 let radius = linear_distance(&[circle_center.center[0], circle_center.center[1]], p1); | ||||
|                 let center_point = [circle_center.center[0], circle_center.center[1]]; | ||||
|                 let circle = crate::std::utils::calculate_circle_from_3_points([*p1, *p2, *p3]); | ||||
|                 let center_point = [circle.center[0], circle.center[1]]; | ||||
|                 GetTangentialInfoFromPathsResult::Circle { | ||||
|                     center: center_point, | ||||
|                     // Note: a circle is always ccw regardless of the order of points | ||||
|                     ccw: true, | ||||
|                     radius, | ||||
|                     radius: circle.radius, | ||||
|                 } | ||||
|             } | ||||
|             Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => { | ||||
|  | ||||
| @ -365,7 +365,7 @@ impl ProgramMemory { | ||||
|         } | ||||
|  | ||||
|         Err(KclError::UndefinedValue(KclErrorDetails { | ||||
|             message: format!("memory item key `{}` is not defined", var), | ||||
|             message: format!("`{}` is not defined", var), | ||||
|             source_ranges: vec![source_range], | ||||
|         })) | ||||
|     } | ||||
| @ -486,7 +486,7 @@ impl ProgramMemory { | ||||
|         } | ||||
|  | ||||
|         Err(KclError::UndefinedValue(KclErrorDetails { | ||||
|             message: format!("memory item key `{}` is not defined", var), | ||||
|             message: format!("`{}` is not defined", var), | ||||
|             source_ranges: vec![], | ||||
|         })) | ||||
|     } | ||||
|  | ||||
| @ -95,8 +95,7 @@ pub struct DefaultPlanes { | ||||
| pub struct TagIdentifier { | ||||
|     pub value: String, | ||||
|     // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info | ||||
|     // was written. Note that there might be multiple versions of tag info from the same epoch, the version with | ||||
|     // the higher index will be the most recent. | ||||
|     // was written. | ||||
|     #[serde(skip)] | ||||
|     pub info: Vec<(usize, TagEngineInfo)>, | ||||
|     #[serde(skip)] | ||||
| @ -123,10 +122,16 @@ impl TagIdentifier { | ||||
|     /// Add info from a different instance of this tag. | ||||
|     pub fn merge_info(&mut self, other: &TagIdentifier) { | ||||
|         assert_eq!(&self.value, &other.value); | ||||
|         'new_info: for (oe, ot) in &other.info { | ||||
|             for (e, _) in &self.info { | ||||
|                 if e > oe { | ||||
|                     continue 'new_info; | ||||
|         for (oe, ot) in &other.info { | ||||
|             if let Some((e, t)) = self.info.last_mut() { | ||||
|                 // If there is newer info, then skip this iteration. | ||||
|                 if *e > *oe { | ||||
|                     continue; | ||||
|                 } | ||||
|                 // If we're in the same epoch, then overwrite. | ||||
|                 if e == oe { | ||||
|                     *t = ot.clone(); | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|             self.info.push((*oe, ot.clone())); | ||||
| @ -1585,7 +1590,7 @@ const answer = returnX()"#; | ||||
|         assert_eq!( | ||||
|             err, | ||||
|             KclError::UndefinedValue(KclErrorDetails { | ||||
|                 message: "memory item key `x` is not defined".to_owned(), | ||||
|                 message: "`x` is not defined".to_owned(), | ||||
|                 source_ranges: vec![ | ||||
|                     SourceRange::new(64, 65, ModuleId::default()), | ||||
|                     SourceRange::new(97, 106, ModuleId::default()) | ||||
| @ -1669,7 +1674,7 @@ let shape = layer() |> patternTransform(instances = 10, transform = transform) | ||||
|         assert_eq!( | ||||
|             err, | ||||
|             KclError::UndefinedValue(KclErrorDetails { | ||||
|                 message: "memory item key `x` is not defined".to_owned(), | ||||
|                 message: "`x` is not defined".to_owned(), | ||||
|                 source_ranges: vec![SourceRange::new(80, 81, ModuleId::default())], | ||||
|             }), | ||||
|         ); | ||||
| @ -1830,18 +1835,18 @@ const bracket = startSketchOn(XY) | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_unary_operator_not_succeeds() { | ||||
|         let ast = r#" | ||||
| fn returnTrue = () => { return !false } | ||||
| const t = true | ||||
| const f = false | ||||
| let notTrue = !t | ||||
| let notFalse = !f | ||||
| let c = !!true | ||||
| let d = !returnTrue() | ||||
| fn returnTrue() { return !false } | ||||
| t = true | ||||
| f = false | ||||
| notTrue = !t | ||||
| notFalse = !f | ||||
| c = !!true | ||||
| d = !returnTrue() | ||||
|  | ||||
| assert(!false, "expected to pass") | ||||
| assertIs(!false, error = "expected to pass") | ||||
|  | ||||
| fn check = (x) => { | ||||
|   assert(!x, "expected argument to be false") | ||||
|   assertIs(!x, error = "expected argument to be false") | ||||
|   return true | ||||
| } | ||||
| check(false) | ||||
|  | ||||
| @ -18,16 +18,6 @@ use crate::{ | ||||
|     CompilationError, SourceRange, | ||||
| }; | ||||
|  | ||||
| lazy_static::lazy_static! { | ||||
|     pub(crate) static ref CHECK_NUMERIC_TYPES: bool = { | ||||
|         let env_var = std::env::var("ZOO_NUM_TYS"); | ||||
|         let Ok(env_var) = env_var else { | ||||
|             return false; | ||||
|         }; | ||||
|         !env_var.is_empty() | ||||
|     }; | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, PartialEq)] | ||||
| pub enum RuntimeType { | ||||
|     Primitive(PrimitiveType), | ||||
| @ -62,10 +52,18 @@ impl RuntimeType { | ||||
|         RuntimeType::Primitive(PrimitiveType::Solid) | ||||
|     } | ||||
|  | ||||
|     pub fn helix() -> Self { | ||||
|         RuntimeType::Primitive(PrimitiveType::Helix) | ||||
|     } | ||||
|  | ||||
|     pub fn plane() -> Self { | ||||
|         RuntimeType::Primitive(PrimitiveType::Plane) | ||||
|     } | ||||
|  | ||||
|     pub fn bool() -> Self { | ||||
|         RuntimeType::Primitive(PrimitiveType::Boolean) | ||||
|     } | ||||
|  | ||||
|     pub fn string() -> Self { | ||||
|         RuntimeType::Primitive(PrimitiveType::String) | ||||
|     } | ||||
| @ -90,6 +88,10 @@ impl RuntimeType { | ||||
|         )))) | ||||
|     } | ||||
|  | ||||
|     pub fn known_length(len: UnitLen) -> Self { | ||||
|         RuntimeType::Primitive(PrimitiveType::Number(NumericType::Known(UnitType::Length(len)))) | ||||
|     } | ||||
|  | ||||
|     pub fn angle() -> Self { | ||||
|         RuntimeType::Primitive(PrimitiveType::Number(NumericType::Known(UnitType::Angle( | ||||
|             UnitAngle::Unknown, | ||||
| @ -366,6 +368,7 @@ impl fmt::Display for PrimitiveType { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"), | ||||
|             PrimitiveType::Number(NumericType::Unknown) => write!(f, "number(unknown units)"), | ||||
|             PrimitiveType::Number(_) => write!(f, "number"), | ||||
|             PrimitiveType::String => write!(f, "string"), | ||||
|             PrimitiveType::Boolean => write!(f, "bool"), | ||||
| @ -423,13 +426,61 @@ impl NumericType { | ||||
|         NumericType::Known(UnitType::Angle(UnitAngle::Degrees)) | ||||
|     } | ||||
|  | ||||
|     /// Combine two types when we expect them to be equal. | ||||
|     pub fn expect_default_length(&self) -> Self { | ||||
|         match self { | ||||
|             NumericType::Default { len, .. } => NumericType::Known(UnitType::Length(*len)), | ||||
|             _ => unreachable!(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn expect_default_angle(&self) -> Self { | ||||
|         match self { | ||||
|             NumericType::Default { angle, .. } => NumericType::Known(UnitType::Angle(*angle)), | ||||
|             _ => unreachable!(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Combine two types when we expect them to be equal, erring on the side of less coercion. To be | ||||
|     /// precise, only adjusting one number or the other when they are of known types. | ||||
|     /// | ||||
|     /// This combinator function is suitable for comparisons or arithmetic where uncertainty should | ||||
|     /// be handled by the user. | ||||
|     pub fn combine_eq(a: TyF64, b: TyF64) -> (f64, f64, NumericType) { | ||||
|         use NumericType::*; | ||||
|         match (a.ty, b.ty) { | ||||
|             (at, bt) if at == bt => (a.n, b.n, at), | ||||
|             (at, Any) => (a.n, b.n, at), | ||||
|             (Any, bt) => (a.n, b.n, bt), | ||||
|  | ||||
|             (t @ Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => (a.n, l2.adjust_to(b.n, l1).0, t), | ||||
|             (t @ Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => (a.n, a2.adjust_to(b.n, a1).0, t), | ||||
|  | ||||
|             (Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => { | ||||
|                 (a.n, b.n, Known(UnitType::Count)) | ||||
|             } | ||||
|             (t @ Known(UnitType::Length(l1)), Default { len: l2, .. }) if l1 == l2 => (a.n, b.n, t), | ||||
|             (Default { len: l1, .. }, t @ Known(UnitType::Length(l2))) if l1 == l2 => (a.n, b.n, t), | ||||
|             (t @ Known(UnitType::Angle(a1)), Default { angle: a2, .. }) if a1 == a2 => (a.n, b.n, t), | ||||
|             (Default { angle: a1, .. }, t @ Known(UnitType::Angle(a2))) if a1 == a2 => (a.n, b.n, t), | ||||
|  | ||||
|             _ => (a.n, b.n, Unknown), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Combine two types when we expect them to be equal, erring on the side of more coercion. Including adjusting when | ||||
|     /// we are certain about only one type. | ||||
|     /// | ||||
|     /// This combinator function is suitable for situations where the user would almost certainly want the types to be | ||||
|     /// coerced together, for example two arguments to the same function or two numbers in an array being used as a point. | ||||
|     /// | ||||
|     /// Prefer to use `combine_eq` if possible since using that prioritises correctness over ergonomics. | ||||
|     pub fn combine_eq_coerce(a: TyF64, b: TyF64) -> (f64, f64, NumericType) { | ||||
|         use NumericType::*; | ||||
|         match (a.ty, b.ty) { | ||||
|             (at, bt) if at == bt => (a.n, b.n, at), | ||||
|             (at, Any) => (a.n, b.n, at), | ||||
|             (Any, bt) => (a.n, b.n, bt), | ||||
|  | ||||
|             (Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => (a.n, b.n, Unknown), | ||||
|  | ||||
|             // Known types and compatible, but needs adjustment. | ||||
| @ -454,12 +505,9 @@ impl NumericType { | ||||
|  | ||||
|     pub fn combine_eq_array(input: &[TyF64]) -> (Vec<f64>, NumericType) { | ||||
|         use NumericType::*; | ||||
|  | ||||
|         let mut result = input.iter().map(|t| t.n).collect(); | ||||
|         let result = input.iter().map(|t| t.n).collect(); | ||||
|  | ||||
|         let mut ty = Any; | ||||
|         // Invariant mismatch is true => ty is fully known | ||||
|         let mut mismatch = false; | ||||
|         for i in input { | ||||
|             if i.ty == Any || ty == i.ty { | ||||
|                 continue; | ||||
| @ -471,58 +519,24 @@ impl NumericType { | ||||
|                 } | ||||
|                 (_, Unknown) | (Default { .. }, Default { .. }) => return (result, Unknown), | ||||
|  | ||||
|                 // Known types and compatible, but needs adjustment. | ||||
|                 (Known(UnitType::Length(_)), Known(UnitType::Length(_))) | ||||
|                 | (Known(UnitType::Angle(_)), Known(UnitType::Angle(_))) => { | ||||
|                     mismatch = true; | ||||
|                 } | ||||
|  | ||||
|                 // Known but incompatible. | ||||
|                 (Known(_), Known(_)) => return (result, Unknown), | ||||
|  | ||||
|                 // Known and unknown, no adjustment for counting numbers. | ||||
|                 (Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => { | ||||
|                     ty = Known(UnitType::Count); | ||||
|                 } | ||||
|  | ||||
|                 (Known(UnitType::Length(l1)), Default { len: l2, .. }) => { | ||||
|                     mismatch |= l1 != l2; | ||||
|                 } | ||||
|                 (Known(UnitType::Angle(a1)), Default { angle: a2, .. }) => { | ||||
|                     mismatch |= a1 != a2; | ||||
|                 } | ||||
|                 (Known(UnitType::Length(l1)), Default { len: l2, .. }) if l1 == l2 => {} | ||||
|                 (Known(UnitType::Angle(a1)), Default { angle: a2, .. }) if a1 == a2 => {} | ||||
|  | ||||
|                 (Default { len: l1, .. }, Known(UnitType::Length(l2))) => { | ||||
|                     mismatch |= l1 != l2; | ||||
|                 (Default { len: l1, .. }, Known(UnitType::Length(l2))) if l1 == l2 => { | ||||
|                     ty = Known(UnitType::Length(*l2)); | ||||
|                 } | ||||
|                 (Default { angle: a1, .. }, Known(UnitType::Angle(a2))) => { | ||||
|                     mismatch |= a1 != a2; | ||||
|                 (Default { angle: a1, .. }, Known(UnitType::Angle(a2))) if a1 == a2 => { | ||||
|                     ty = Known(UnitType::Angle(*a2)); | ||||
|                 } | ||||
|  | ||||
|                 (Unknown, _) | (_, Any) => unreachable!(), | ||||
|                 _ => return (result, Unknown), | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if !mismatch { | ||||
|             return (result, ty); | ||||
|         } | ||||
|  | ||||
|         result = result | ||||
|             .into_iter() | ||||
|             .zip(input) | ||||
|             .map(|(n, i)| match (&ty, &i.ty) { | ||||
|                 (Known(UnitType::Length(l1)), Known(UnitType::Length(l2)) | Default { len: l2, .. }) => { | ||||
|                     l2.adjust_to(n, *l1).0 | ||||
|                 } | ||||
|                 (Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2)) | Default { angle: a2, .. }) => { | ||||
|                     a2.adjust_to(n, *a1).0 | ||||
|                 } | ||||
|                 _ => unreachable!(), | ||||
|             }) | ||||
|             .collect(); | ||||
|  | ||||
|         (result, ty) | ||||
|     } | ||||
|  | ||||
| @ -530,11 +544,11 @@ impl NumericType { | ||||
|     pub fn combine_mul(a: TyF64, b: TyF64) -> (f64, f64, NumericType) { | ||||
|         use NumericType::*; | ||||
|         match (a.ty, b.ty) { | ||||
|             (at @ Default { .. }, bt @ Default { .. }) if at != bt => (a.n, b.n, Unknown), | ||||
|             (at @ Default { .. }, bt @ Default { .. }) if at == bt => (a.n, b.n, at), | ||||
|             (Default { .. }, Default { .. }) => (a.n, b.n, Unknown), | ||||
|             (Known(UnitType::Count), bt) => (a.n, b.n, bt), | ||||
|             (at, Known(UnitType::Count)) => (a.n, b.n, at), | ||||
|             (Default { .. }, bt) => (a.n, b.n, bt), | ||||
|             (at, Default { .. }) => (a.n, b.n, at), | ||||
|             (at @ Known(_), Default { .. }) | (Default { .. }, at @ Known(_)) => (a.n, b.n, at), | ||||
|             (Any, Any) => (a.n, b.n, Any), | ||||
|             _ => (a.n, b.n, Unknown), | ||||
|         } | ||||
| @ -548,18 +562,7 @@ impl NumericType { | ||||
|             (at, bt) if at == bt => (a.n, b.n, Known(UnitType::Count)), | ||||
|             (Default { .. }, Default { .. }) => (a.n, b.n, Unknown), | ||||
|             (at, Known(UnitType::Count) | Any) => (a.n, b.n, at), | ||||
|             (Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => { | ||||
|                 (a.n, l2.adjust_to(b.n, l1).0, Known(UnitType::Count)) | ||||
|             } | ||||
|             (Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => { | ||||
|                 (a.n, a2.adjust_to(b.n, a1).0, Known(UnitType::Count)) | ||||
|             } | ||||
|             (Default { len: l1, .. }, Known(UnitType::Length(l2))) => { | ||||
|                 (l1.adjust_to(a.n, l2).0, b.n, Known(UnitType::Count)) | ||||
|             } | ||||
|             (Default { angle: a1, .. }, Known(UnitType::Angle(a2))) => { | ||||
|                 (a1.adjust_to(a.n, a2).0, b.n, Known(UnitType::Count)) | ||||
|             } | ||||
|             (at @ Known(_), Default { .. }) => (a.n, b.n, at), | ||||
|             (Known(UnitType::Count), _) => (a.n, b.n, Known(UnitType::Count)), | ||||
|             _ => (a.n, b.n, Unknown), | ||||
|         } | ||||
| @ -604,9 +607,18 @@ impl NumericType { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn is_unknown(&self) -> bool { | ||||
|         matches!( | ||||
|             self, | ||||
|             NumericType::Unknown | ||||
|                 | NumericType::Known(UnitType::Angle(UnitAngle::Unknown)) | ||||
|                 | NumericType::Known(UnitType::Length(UnitLen::Unknown)) | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     fn example_ty(&self) -> Option<String> { | ||||
|         match self { | ||||
|             Self::Known(t) => Some(t.to_string()), | ||||
|             Self::Known(t) if !self.is_unknown() => Some(t.to_string()), | ||||
|             Self::Default { len, .. } => Some(len.to_string()), | ||||
|             _ => None, | ||||
|         } | ||||
| @ -617,10 +629,6 @@ impl NumericType { | ||||
|             return Err(val.into()); | ||||
|         }; | ||||
|  | ||||
|         if !*CHECK_NUMERIC_TYPES { | ||||
|             return Ok(val.clone()); | ||||
|         } | ||||
|  | ||||
|         if ty.subtype(self) { | ||||
|             return Ok(KclValue::Number { | ||||
|                 value: *value, | ||||
| @ -771,10 +779,10 @@ pub enum UnitLen { | ||||
| } | ||||
|  | ||||
| impl UnitLen { | ||||
|     fn adjust_to(self, value: f64, to: UnitLen) -> (f64, UnitLen) { | ||||
|     pub fn adjust_to(self, value: f64, to: UnitLen) -> (f64, UnitLen) { | ||||
|         use UnitLen::*; | ||||
|  | ||||
|         if !*CHECK_NUMERIC_TYPES || self == to { | ||||
|         if self == to { | ||||
|             return (value, to); | ||||
|         } | ||||
|  | ||||
| @ -894,15 +902,11 @@ pub enum UnitAngle { | ||||
| } | ||||
|  | ||||
| impl UnitAngle { | ||||
|     fn adjust_to(self, value: f64, to: UnitAngle) -> (f64, UnitAngle) { | ||||
|     pub fn adjust_to(self, value: f64, to: UnitAngle) -> (f64, UnitAngle) { | ||||
|         use std::f64::consts::PI; | ||||
|  | ||||
|         use UnitAngle::*; | ||||
|  | ||||
|         if !*CHECK_NUMERIC_TYPES { | ||||
|             return (value, to); | ||||
|         } | ||||
|  | ||||
|         if to == Unknown { | ||||
|             return (value, self); | ||||
|         } | ||||
| @ -1053,8 +1057,6 @@ impl KclValue { | ||||
|                         y_axis, | ||||
|                         z_axis, | ||||
|                         value: super::PlaneType::Uninit, | ||||
|                         // TODO use length unit from origin | ||||
|                         units: exec_state.length_unit(), | ||||
|                         meta: meta.clone(), | ||||
|                     }; | ||||
|  | ||||
| @ -1948,10 +1950,6 @@ mod test { | ||||
|         assert_coerce_results(&unknown, &NumericType::Any.into(), &unknown, &mut exec_state); | ||||
|         assert_coerce_results(&default, &NumericType::Any.into(), &default, &mut exec_state); | ||||
|  | ||||
|         if !*CHECK_NUMERIC_TYPES { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         assert_eq!( | ||||
|             default | ||||
|                 .coerce( | ||||
| @ -2064,20 +2062,14 @@ u = min(3rad, 4in) | ||||
| "#; | ||||
|  | ||||
|         let result = parse_execute(program).await.unwrap(); | ||||
|         if *CHECK_NUMERIC_TYPES { | ||||
|             assert_eq!(result.exec_state.errors().len(), 3); | ||||
|         } else { | ||||
|             assert!(result.exec_state.errors().is_empty()); | ||||
|         } | ||||
|         assert_eq!(result.exec_state.errors().len(), 5); | ||||
|  | ||||
|         assert_value_and_type("a", &result, 9.0, NumericType::default()); | ||||
|         assert_value_and_type("b", &result, 3.0, NumericType::default()); | ||||
|         assert_value_and_type("c", &result, 13.0, NumericType::mm()); | ||||
|         assert_value_and_type("d", &result, 13.0, NumericType::mm()); | ||||
|         assert_value_and_type("e", &result, 13.0, NumericType::mm()); | ||||
|         if *CHECK_NUMERIC_TYPES { | ||||
|             assert_value_and_type("f", &result, 5.0, NumericType::mm()); | ||||
|         } | ||||
|         assert_value_and_type("f", &result, 5.0, NumericType::mm()); | ||||
|  | ||||
|         assert_value_and_type("g", &result, 20.0, NumericType::default()); | ||||
|         assert_value_and_type("h", &result, 20.0, NumericType::mm()); | ||||
| @ -2087,16 +2079,14 @@ u = min(3rad, 4in) | ||||
|  | ||||
|         assert_value_and_type("l", &result, 0.0, NumericType::default()); | ||||
|         assert_value_and_type("m", &result, 2.0, NumericType::count()); | ||||
|         if *CHECK_NUMERIC_TYPES { | ||||
|             assert_value_and_type("n", &result, 127.0, NumericType::count()); | ||||
|         } | ||||
|         assert_value_and_type("o", &result, 1.0, NumericType::Unknown); | ||||
|         assert_value_and_type("n", &result, 5.0, NumericType::Unknown); | ||||
|         assert_value_and_type("o", &result, 1.0, NumericType::mm()); | ||||
|         assert_value_and_type("p", &result, 1.0, NumericType::count()); | ||||
|         assert_value_and_type("q", &result, 2.0, NumericType::Known(UnitType::Length(UnitLen::Inches))); | ||||
|  | ||||
|         assert_value_and_type("r", &result, 0.0, NumericType::default()); | ||||
|         assert_value_and_type("s", &result, -42.0, NumericType::mm()); | ||||
|         assert_value_and_type("t", &result, 3.0, NumericType::Known(UnitType::Length(UnitLen::Inches))); | ||||
|         assert_value_and_type("t", &result, 3.0, NumericType::Unknown); | ||||
|         assert_value_and_type("u", &result, 3.0, NumericType::Unknown); | ||||
|     } | ||||
|  | ||||
| @ -2110,7 +2100,24 @@ b = 180 / PI * a + 360 | ||||
|         let result = parse_execute(program).await.unwrap(); | ||||
|  | ||||
|         assert_value_and_type("a", &result, 1.0, NumericType::radians()); | ||||
|         // TODO type is not ideal | ||||
|         assert_value_and_type("b", &result, 417.0, NumericType::radians()); | ||||
|         assert_value_and_type("b", &result, 417.0, NumericType::Unknown); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn cos_coercions() { | ||||
|         let program = r#" | ||||
| a = cos(toRadians(30)) | ||||
| b = 3 / a | ||||
| c = cos(30deg) | ||||
| d = cos(30) | ||||
| "#; | ||||
|  | ||||
|         let result = parse_execute(program).await.unwrap(); | ||||
|         assert_eq!(result.exec_state.errors().len(), 1); | ||||
|  | ||||
|         assert_value_and_type("a", &result, 1.0, NumericType::count()); | ||||
|         assert_value_and_type("b", &result, 3.0, NumericType::default()); | ||||
|         assert_value_and_type("c", &result, 1.0, NumericType::count()); | ||||
|         assert_value_and_type("d", &result, 0.0, NumericType::count()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -90,6 +90,7 @@ pub(crate) fn read_std(mod_name: &str) -> Option<&'static str> { | ||||
|         "math" => Some(include_str!("../std/math.kcl")), | ||||
|         "sketch" => Some(include_str!("../std/sketch.kcl")), | ||||
|         "turns" => Some(include_str!("../std/turns.kcl")), | ||||
|         "types" => Some(include_str!("../std/types.kcl")), | ||||
|         _ => None, | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| use sha2::{Digest as DigestTrait, Sha256}; | ||||
|  | ||||
| use crate::parsing::ast::types::{ | ||||
|     Annotation, ArrayExpression, ArrayRangeExpression, Ascription, BinaryExpression, BinaryPart, BodyItem, | ||||
|     Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryPart, BodyItem, | ||||
|     CallExpression, CallExpressionKw, DefaultParamVal, ElseIf, Expr, ExpressionStatement, FunctionExpression, | ||||
|     Identifier, IfExpression, ImportItem, ImportSelector, ImportStatement, ItemVisibility, KclNone, LabelledExpression, | ||||
|     Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name, ObjectExpression, ObjectProperty, | ||||
| @ -464,7 +464,7 @@ impl LabelledExpression { | ||||
|     }); | ||||
| } | ||||
|  | ||||
| impl Ascription { | ||||
| impl AscribedExpression { | ||||
|     compute_digest!(|slf, hasher| { | ||||
|         hasher.update(slf.expr.compute_digest()); | ||||
|         hasher.update(slf.ty.compute_digest()); | ||||
|  | ||||
| @ -450,7 +450,6 @@ impl Program { | ||||
|  | ||||
|         for item in &self.body { | ||||
|             let r = item.comment_range(); | ||||
|             eprintln!("item {r:?}"); | ||||
|             if pos >= r.0 && pos < r.1 { | ||||
|                 return true; | ||||
|             } | ||||
| @ -820,7 +819,7 @@ pub enum Expr { | ||||
|     UnaryExpression(BoxNode<UnaryExpression>), | ||||
|     IfExpression(BoxNode<IfExpression>), | ||||
|     LabelledExpression(BoxNode<LabelledExpression>), | ||||
|     AscribedExpression(BoxNode<Ascription>), | ||||
|     AscribedExpression(BoxNode<AscribedExpression>), | ||||
|     None(Node<KclNone>), | ||||
| } | ||||
|  | ||||
| @ -1093,7 +1092,7 @@ impl LabelledExpression { | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[ts(export)] | ||||
| #[serde(tag = "type")] | ||||
| pub struct Ascription { | ||||
| pub struct AscribedExpression { | ||||
|     pub expr: Expr, | ||||
|     pub ty: Node<Type>, | ||||
|  | ||||
| @ -1102,12 +1101,12 @@ pub struct Ascription { | ||||
|     pub digest: Option<Digest>, | ||||
| } | ||||
|  | ||||
| impl Ascription { | ||||
|     pub(crate) fn new(expr: Expr, ty: Node<Type>) -> Node<Ascription> { | ||||
| impl AscribedExpression { | ||||
|     pub(crate) fn new(expr: Expr, ty: Node<Type>) -> Node<AscribedExpression> { | ||||
|         let start = expr.start(); | ||||
|         let end = ty.end; | ||||
|         let module_id = expr.module_id(); | ||||
|         Node::new(Ascription { expr, ty, digest: None }, start, end, module_id) | ||||
|         Node::new(AscribedExpression { expr, ty, digest: None }, start, end, module_id) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3081,7 +3080,7 @@ impl PipeExpression { | ||||
| #[allow(clippy::large_enum_variant)] | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[ts(export)] | ||||
| #[serde(tag = "type")] | ||||
| #[serde(tag = "p_type")] | ||||
| pub enum PrimitiveType { | ||||
|     /// A string type. | ||||
|     String, | ||||
|  | ||||
| @ -14,7 +14,7 @@ use winnow::{ | ||||
| }; | ||||
|  | ||||
| use super::{ | ||||
|     ast::types::{Ascription, ImportPath, LabelledExpression}, | ||||
|     ast::types::{AscribedExpression, ImportPath, LabelledExpression}, | ||||
|     token::{NumericSuffix, RESERVED_WORDS}, | ||||
|     DeprecationKind, | ||||
| }; | ||||
| @ -530,13 +530,6 @@ pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> PResult<Node<Litera | ||||
|                     CompilationError::fatal(token.as_source_range(), format!("Invalid float: {}", token.value)) | ||||
|                 })?; | ||||
|  | ||||
|                 if token.numeric_suffix().is_some() { | ||||
|                     ParseContext::warn(CompilationError::err( | ||||
|                         (&token).into(), | ||||
|                         "Unit of Measure suffixes are experimental and currently do nothing.", | ||||
|                     )); | ||||
|                 } | ||||
|  | ||||
|                 Ok(( | ||||
|                     LiteralValue::Number { | ||||
|                         value, | ||||
| @ -1635,6 +1628,12 @@ fn function_body(i: &mut TokenSlice) -> PResult<Node<Program>> { | ||||
|                 } | ||||
|                 handle_pending_non_code!(attr); | ||||
|                 if attr.is_inner() { | ||||
|                     if !body.is_empty() { | ||||
|                         ParseContext::warn(CompilationError::err( | ||||
|                             attr.as_source_range(), | ||||
|                             "Named attributes should appear before any declarations or expressions.\n\nBecause named attributes apply to the whole function or module, including code written before them, it can be confusing for readers to not have these attributes at the top of code blocks.", | ||||
|                         )); | ||||
|                     } | ||||
|                     inner_attrs.push(attr); | ||||
|                 } else { | ||||
|                     pending_attrs.push(attr); | ||||
| @ -2015,9 +2014,7 @@ fn expression_but_not_pipe(i: &mut TokenSlice) -> PResult<Expr> { | ||||
|  | ||||
|     let ty = opt((colon, opt(whitespace), argument_type)).parse_next(i)?; | ||||
|     if let Some((_, _, ty)) = ty { | ||||
|         ParseContext::warn(CompilationError::err((&ty).into(), "Type ascription is experimental.")); | ||||
|  | ||||
|         expr = Expr::AscribedExpression(Box::new(Ascription::new(expr, ty))) | ||||
|         expr = Expr::AscribedExpression(Box::new(AscribedExpression::new(expr, ty))) | ||||
|     } | ||||
|     let label = opt(label).parse_next(i)?; | ||||
|     match label { | ||||
| @ -2811,13 +2808,6 @@ fn primitive_type(i: &mut TokenSlice) -> PResult<Node<PrimitiveType>> { | ||||
|     let mut result = Node::new(PrimitiveType::Boolean, ident.start, ident.end, ident.module_id); | ||||
|     result.inner = PrimitiveType::primitive_from_str(&ident.name, suffix).unwrap_or(PrimitiveType::Named(ident)); | ||||
|  | ||||
|     if suffix.is_some() { | ||||
|         ParseContext::warn(CompilationError::err( | ||||
|             result.as_source_range(), | ||||
|             "Unit of Measure types are experimental and currently do nothing.", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     Ok(result) | ||||
| } | ||||
|  | ||||
| @ -4528,23 +4518,32 @@ export fn cos(num: number(rad)): number(_) {}"#; | ||||
|         assert_eq!(errs.len(), 1, "{errs:#?}"); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn warn_late_settings() { | ||||
|         let some_program_string = r#"foo = 42 | ||||
| @settings(defaultLengthUnit = mm) | ||||
| "#; | ||||
|         let (_, errs) = assert_no_err(some_program_string); | ||||
|         assert_eq!(errs.len(), 1, "{errs:#?}"); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn fn_decl_uom_ty() { | ||||
|         let some_program_string = r#"fn foo(x: number(mm)): number(_) { return 1 }"#; | ||||
|         let (_, errs) = assert_no_fatal(some_program_string); | ||||
|         assert_eq!(errs.len(), 2); | ||||
|         assert!(errs.is_empty(), "Expected no errors, found: {errs:?}"); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn error_underscore() { | ||||
|         let (_, errs) = assert_no_fatal("_foo(_blah, _)"); | ||||
|         assert_eq!(errs.len(), 3, "found: {:#?}", errs); | ||||
|         assert_eq!(errs.len(), 3, "found: {errs:#?}"); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn error_type_ascription() { | ||||
|         let (_, errs) = assert_no_fatal("a + b: number"); | ||||
|         assert_eq!(errs.len(), 1, "found: {:#?}", errs); | ||||
|         assert!(errs.is_empty()); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|  | ||||
| @ -2600,3 +2600,24 @@ mod import_async { | ||||
|         super::execute(TEST_NAME, true).await | ||||
|     } | ||||
| } | ||||
| mod loop_tag { | ||||
|     const TEST_NAME: &str = "loop_tag"; | ||||
|  | ||||
|     /// Test parsing KCL. | ||||
|     #[test] | ||||
|     fn parse() { | ||||
|         super::parse(TEST_NAME) | ||||
|     } | ||||
|  | ||||
|     /// Test that parsing and unparsing KCL produces the original KCL input. | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn unparse() { | ||||
|         super::unparse(TEST_NAME).await | ||||
|     } | ||||
|  | ||||
|     /// Test that KCL is executed correctly. | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn kcl_test_execute() { | ||||
|         super::execute(TEST_NAME, true).await | ||||
|     } | ||||
| } | ||||
|  | ||||