Skip to content

Commit

Permalink
feat: unsafeCast builtin function (#4483)
Browse files Browse the repository at this point in the history
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: #4161 (comment))

## 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)*.
  • Loading branch information
Chriscbr authored Oct 10, 2023
1 parent 95a6b30 commit 4ec835e
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 7 deletions.
9 changes: 5 additions & 4 deletions docs/docs/03-language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
12 changes: 12 additions & 0 deletions examples/tests/valid/casting.test.w
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 7 additions & 1 deletion libs/wingc/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,12 +318,17 @@ pub struct Stmt {
pub enum UtilityFunctions {
Log,
Assert,
UnsafeCast,
}

impl UtilityFunctions {
/// Returns all utility functions.
pub fn all() -> Vec<UtilityFunctions> {
vec![UtilityFunctions::Log, UtilityFunctions::Assert]
vec![
UtilityFunctions::Log,
UtilityFunctions::Assert,
UtilityFunctions::UnsafeCast,
]
}
}

Expand All @@ -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"),
}
}
}
Expand Down
20 changes: 19 additions & 1 deletion libs/wingc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions libs/wingc/src/lsp/snapshots/completions/empty.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {}"
Expand Down
12 changes: 12 additions & 0 deletions libs/wingc/src/lsp/snapshots/completions/struct_literal_value.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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();

```
Original file line number Diff line number Diff line change
@@ -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 <DURATION>
```

Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 4ec835e

Please sign in to comment.