From 4ec835ec66a4ecd9a0d5d1e1e5aca2c3b586d290 Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Tue, 10 Oct 2023 07:56:19 -0400 Subject: [PATCH] feat: unsafeCast builtin function (#4483) Fixes #4161 For situations where application code needs to interact with foreign interfaces, or where the compiler cannot infer all type information, most modern languages support some mechanism for "casting" or performing unsafe conversions between types ([1](https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/casting-between-types.html), [2](https://pkg.go.dev/unsafe#Pointer), [3](https://www.baeldung.com/java-type-casting), [4](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/typecasting/)). Indeed, we've already identified some use cases in #4161 that require casting, like performing low-level escape hatches in CDK applications. To that end, this PR introduces a global function named `unsafeCast` that allows performing type casting. Below is an example where `unsafeCast` is used to override a piece of low-level Terraform configuration created by `cloud.Bucket`: ```js bring cloud; bring util; bring "@cdktf/provider-aws" as aws; let b = new cloud.Bucket(); if util.env("WING_TARGET") == "tf-aws" { let s3Bucket: aws.s3Bucket.S3Bucket = unsafeCast(b.node.findChild("Default")); s3Bucket.addOverride("bucket_prefix", "my-prefix-"); log(s3Bucket.node.path); } ``` `unsafeCast` is experimental and subject to change. Prior to Wing 1.0, it may be worthwhile to improve support for casting in (at least) two ways: 1. Upgrade `unsafeCast` into a dedicated piece of language syntax. 2. Limit the kinds of casts that are allowed (see comment: https://github.com/winglang/wing/issues/4161#issuecomment-1754162808) ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [x] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- docs/docs/03-language-reference.md | 9 ++- examples/tests/valid/casting.test.w | 12 +++ libs/wingc/src/ast.rs | 8 +- libs/wingc/src/lib.rs | 20 ++++- .../completions/call_struct_expansion.snap | 12 +++ .../call_struct_expansion_partial.snap | 12 +++ .../src/lsp/snapshots/completions/empty.snap | 12 +++ .../only_show_symbols_in_scope.snap | 12 +++ .../completions/struct_literal_value.snap | 12 +++ .../valid/casting.test.w_compile_tf-aws.md | 79 +++++++++++++++++++ .../valid/casting.test.w_test_sim.md | 12 +++ .../valid/debug_env.test.w_test_sim.md | 2 +- 12 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 examples/tests/valid/casting.test.w create mode 100644 tools/hangar/__snapshots__/test_corpus/valid/casting.test.w_compile_tf-aws.md create mode 100644 tools/hangar/__snapshots__/test_corpus/valid/casting.test.w_test_sim.md diff --git a/docs/docs/03-language-reference.md b/docs/docs/03-language-reference.md index 1b3d1be51b8..d522f445c45 100644 --- a/docs/docs/03-language-reference.md +++ b/docs/docs/03-language-reference.md @@ -538,10 +538,11 @@ log("UTC: ${t1.utc.toIso())}"); // output: 2023-02-09T06:21:03.000Z ### 1.2 Utility Functions -| Name | Extra information | -| -------- | ----------------------------------------------------- | -| `log` | logs str | -| `assert` | checks a condition and _throws_ if evaluated to false | +| Name | Extra information | +| ------------ | ----------------------------------------------------- | +| `log` | logs str | +| `assert` | checks a condition and _throws_ if evaluated to false | +| `unsafeCast` | cast a value into a different type | > ```TS > log("Hello ${name}"); diff --git a/examples/tests/valid/casting.test.w b/examples/tests/valid/casting.test.w new file mode 100644 index 00000000000..97602868438 --- /dev/null +++ b/examples/tests/valid/casting.test.w @@ -0,0 +1,12 @@ +bring cloud; +bring util; +bring "@cdktf/provider-aws" as aws; + +let b = new cloud.Bucket(); + +if util.env("WING_TARGET") == "tf-aws" { + let s3Bucket: aws.s3Bucket.S3Bucket = unsafeCast(b.node.findChild("Default")); + + s3Bucket.addOverride("bucket_prefix", "my-prefix-"); + log(s3Bucket.node.path); +} diff --git a/libs/wingc/src/ast.rs b/libs/wingc/src/ast.rs index cfc519aab2c..37cdbcf81b6 100644 --- a/libs/wingc/src/ast.rs +++ b/libs/wingc/src/ast.rs @@ -318,12 +318,17 @@ pub struct Stmt { pub enum UtilityFunctions { Log, Assert, + UnsafeCast, } impl UtilityFunctions { /// Returns all utility functions. pub fn all() -> Vec { - vec![UtilityFunctions::Log, UtilityFunctions::Assert] + vec![ + UtilityFunctions::Log, + UtilityFunctions::Assert, + UtilityFunctions::UnsafeCast, + ] } } @@ -332,6 +337,7 @@ impl Display for UtilityFunctions { match self { UtilityFunctions::Log => write!(f, "log"), UtilityFunctions::Assert => write!(f, "assert"), + UtilityFunctions::UnsafeCast => write!(f, "unsafeCast"), } } } diff --git a/libs/wingc/src/lib.rs b/libs/wingc/src/lib.rs index 14d1c9fd672..951042835f9 100644 --- a/libs/wingc/src/lib.rs +++ b/libs/wingc/src/lib.rs @@ -110,7 +110,7 @@ const MACRO_REPLACE_SELF: &'static str = "$self$"; const MACRO_REPLACE_ARGS: &'static str = "$args$"; const MACRO_REPLACE_ARGS_TEXT: &'static str = "$args_text$"; -pub const GLOBAL_SYMBOLS: [&'static str; 3] = [WINGSDK_STD_MODULE, "assert", "log"]; +pub const GLOBAL_SYMBOLS: [&'static str; 4] = [WINGSDK_STD_MODULE, "assert", "log", "unsafeCast"]; pub struct CompilerOutput {} @@ -241,6 +241,24 @@ pub fn type_check( scope, types, ); + add_builtin( + UtilityFunctions::UnsafeCast.to_string().as_str(), + Type::Function(FunctionSignature { + this_type: None, + parameters: vec![FunctionParameter { + name: "value".into(), + typeref: types.anything(), + docs: Docs::with_summary("The value to cast into a different type"), + variadic: false, + }], + return_type: types.anything(), + phase: Phase::Independent, + js_override: Some("$args$".to_string()), + docs: Docs::with_summary("Casts a value into a different type. This is unsafe and can cause runtime errors"), + }), + scope, + types, + ); let mut scope_env = types.get_scope_env(&scope); let mut tc = TypeChecker::new(types, file_path, file_graph, jsii_types, jsii_imports); diff --git a/libs/wingc/src/lsp/snapshots/completions/call_struct_expansion.snap b/libs/wingc/src/lsp/snapshots/completions/call_struct_expansion.snap index d244cb0fe84..c1dfba583d9 100644 --- a/libs/wingc/src/lsp/snapshots/completions/call_struct_expansion.snap +++ b/libs/wingc/src/lsp/snapshots/completions/call_struct_expansion.snap @@ -37,6 +37,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: unsafeCast + kind: 3 + detail: "(value: any): any" + documentation: + kind: markdown + value: "```wing\nunsafeCast: (value: any): any\n```\n---\nCasts a value into a different type. This is unsafe and can cause runtime errors\n\n### Parameters\n- `value` — The value to cast into a different type" + sortText: cc|unsafeCast + insertText: unsafeCast($0) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: x kind: 3 detail: "preflight (arg1: A): void" diff --git a/libs/wingc/src/lsp/snapshots/completions/call_struct_expansion_partial.snap b/libs/wingc/src/lsp/snapshots/completions/call_struct_expansion_partial.snap index cda29aaba92..06b7fefd55a 100644 --- a/libs/wingc/src/lsp/snapshots/completions/call_struct_expansion_partial.snap +++ b/libs/wingc/src/lsp/snapshots/completions/call_struct_expansion_partial.snap @@ -37,6 +37,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: unsafeCast + kind: 3 + detail: "(value: any): any" + documentation: + kind: markdown + value: "```wing\nunsafeCast: (value: any): any\n```\n---\nCasts a value into a different type. This is unsafe and can cause runtime errors\n\n### Parameters\n- `value` — The value to cast into a different type" + sortText: cc|unsafeCast + insertText: unsafeCast($0) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: x kind: 3 detail: "preflight (arg1: A): void" diff --git a/libs/wingc/src/lsp/snapshots/completions/empty.snap b/libs/wingc/src/lsp/snapshots/completions/empty.snap index 5308742eb14..96216aeee2c 100644 --- a/libs/wingc/src/lsp/snapshots/completions/empty.snap +++ b/libs/wingc/src/lsp/snapshots/completions/empty.snap @@ -25,6 +25,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: unsafeCast + kind: 3 + detail: "(value: any): any" + documentation: + kind: markdown + value: "```wing\nunsafeCast: (value: any): any\n```\n---\nCasts a value into a different type. This is unsafe and can cause runtime errors\n\n### Parameters\n- `value` — The value to cast into a different type" + sortText: cc|unsafeCast + insertText: unsafeCast($0) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: "inflight () => {}" kind: 15 sortText: "ll|inflight () => {}" diff --git a/libs/wingc/src/lsp/snapshots/completions/only_show_symbols_in_scope.snap b/libs/wingc/src/lsp/snapshots/completions/only_show_symbols_in_scope.snap index 06371d98006..cfbc9e7a024 100644 --- a/libs/wingc/src/lsp/snapshots/completions/only_show_symbols_in_scope.snap +++ b/libs/wingc/src/lsp/snapshots/completions/only_show_symbols_in_scope.snap @@ -39,6 +39,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: unsafeCast + kind: 3 + detail: "(value: any): any" + documentation: + kind: markdown + value: "```wing\nunsafeCast: (value: any): any\n```\n---\nCasts a value into a different type. This is unsafe and can cause runtime errors\n\n### Parameters\n- `value` — The value to cast into a different type" + sortText: cc|unsafeCast + insertText: unsafeCast($0) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: "inflight () => {}" kind: 15 sortText: "ll|inflight () => {}" diff --git a/libs/wingc/src/lsp/snapshots/completions/struct_literal_value.snap b/libs/wingc/src/lsp/snapshots/completions/struct_literal_value.snap index c80fc34772f..1a7ad9bbe8e 100644 --- a/libs/wingc/src/lsp/snapshots/completions/struct_literal_value.snap +++ b/libs/wingc/src/lsp/snapshots/completions/struct_literal_value.snap @@ -25,6 +25,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: unsafeCast + kind: 3 + detail: "(value: any): any" + documentation: + kind: markdown + value: "```wing\nunsafeCast: (value: any): any\n```\n---\nCasts a value into a different type. This is unsafe and can cause runtime errors\n\n### Parameters\n- `value` — The value to cast into a different type" + sortText: cc|unsafeCast + insertText: unsafeCast($0) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: Foo kind: 22 documentation: diff --git a/tools/hangar/__snapshots__/test_corpus/valid/casting.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/casting.test.w_compile_tf-aws.md new file mode 100644 index 00000000000..b03ef3acf05 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/valid/casting.test.w_compile_tf-aws.md @@ -0,0 +1,79 @@ +# [casting.test.w](../../../../../examples/tests/valid/casting.test.w) | compile | tf-aws + +## main.tf.json +```json +{ + "//": { + "metadata": { + "backend": "local", + "overrides": { + "aws_s3_bucket": [ + "bucket_prefix" + ] + }, + "stackName": "root", + "version": "0.17.0" + }, + "outputs": { + "root": { + "Default": { + "cloud.TestRunner": { + "TestFunctionArns": "WING_TEST_RUNNER_FUNCTION_ARNS" + } + } + } + } + }, + "output": { + "WING_TEST_RUNNER_FUNCTION_ARNS": { + "value": "[]" + } + }, + "provider": { + "aws": [ + {} + ] + }, + "resource": { + "aws_s3_bucket": { + "cloudBucket": { + "//": { + "metadata": { + "path": "root/Default/Default/cloud.Bucket/Default", + "uniqueId": "cloudBucket" + } + }, + "bucket_prefix": "my-prefix-", + "force_destroy": false + } + } + } +} +``` + +## preflight.js +```js +const $stdlib = require('@winglang/sdk'); +const $plugins = ((s) => !s ? [] : s.split(';'))(process.env.WING_PLUGIN_PATHS); +const $outdir = process.env.WING_SYNTH_DIR ?? "."; +const $wing_is_test = process.env.WING_IS_TEST === "true"; +const std = $stdlib.std; +const cloud = $stdlib.cloud; +const util = $stdlib.util; +const aws = require("@cdktf/provider-aws"); +class $Root extends $stdlib.std.Resource { + constructor(scope, id) { + super(scope, id); + const b = this.node.root.newAbstract("@winglang/sdk.cloud.Bucket",this,"cloud.Bucket"); + if ((((a,b) => { try { return require('assert').deepStrictEqual(a,b) === undefined; } catch { return false; } })((util.Util.env("WING_TARGET")),"tf-aws"))) { + const s3Bucket = (b.node.findChild("Default")); + (s3Bucket.addOverride("bucket_prefix","my-prefix-")); + {console.log(s3Bucket.node.path)}; + } + } +} +const $App = $stdlib.core.App.for(process.env.WING_TARGET); +new $App({ outdir: $outdir, name: "casting.test", rootConstruct: $Root, plugins: $plugins, isTestEnvironment: $wing_is_test, entrypointDir: process.env['WING_SOURCE_DIR'], rootId: process.env['WING_ROOT_ID'] }).synth(); + +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/valid/casting.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/valid/casting.test.w_test_sim.md new file mode 100644 index 00000000000..f4c2718e970 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/valid/casting.test.w_test_sim.md @@ -0,0 +1,12 @@ +# [casting.test.w](../../../../../examples/tests/valid/casting.test.w) | test | sim + +## stdout.log +```log +pass ─ casting.test.wsim (no tests) + + +Tests 1 passed (1) +Test Files 1 passed (1) +Duration +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md index 9fd46ef43b6..e23d2c1abc2 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md @@ -4,7 +4,7 @@ ```log [symbol environment at ../../../../examples/tests/valid/debug_env.test.w:7:5] level 0: { this => A } -level 1: { A => A [type], assert => (condition: bool): void, cloud => cloud [namespace], log => (message: str): void, std => std [namespace] } +level 1: { A => A [type], assert => (condition: bool): void, cloud => cloud [namespace], log => (message: str): void, std => std [namespace], unsafeCast => (value: any): any } pass ─ debug_env.test.wsim (no tests)