From 56010c4f7c5934e7fabdefb8fd91e8896f624f29 Mon Sep 17 00:00:00 2001 From: yoav-steinberg Date: Fri, 21 Jun 2024 21:17:17 +0300 Subject: [PATCH] feat(compiler): punning (#6752) Fixes #247 Support "punning" in struct and json literals ```wing struct Bla { a: num; b: str; } let a = 1; let b = "bla"; let x = Bla {a, b}; let y: Bla = Json {a,b}; ``` ## 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 | 18 ++++++ examples/tests/invalid/json.test.w | 12 ++++ examples/tests/invalid/structs.test.w | 9 +++ examples/tests/valid/json.test.w | 17 ++++++ examples/tests/valid/structs.test.w | 27 +++++++++ libs/tree-sitter-wing/grammar.js | 5 +- libs/tree-sitter-wing/src/grammar.json | 58 ++++++++++++------- libs/wingc/src/parser.rs | 24 ++++++-- libs/wingc/src/valid_json_visitor.rs | 45 ++++++-------- tools/hangar/__snapshots__/invalid.ts.snap | 56 ++++++++++++++++++ .../valid/json.test.w_compile_tf-aws.md | 11 ++++ .../valid/structs.test.w_compile_tf-aws.md | 9 +++ 12 files changed, 236 insertions(+), 55 deletions(-) diff --git a/docs/docs/03-language-reference.md b/docs/docs/03-language-reference.md index f72c112905d..26dcf996773 100644 --- a/docs/docs/03-language-reference.md +++ b/docs/docs/03-language-reference.md @@ -213,6 +213,13 @@ The `Json` keyword can be omitted from `Json` object literals: let jsonObj = { boom: 123, bam: [4, 5, 6] }; ``` +You may use "punning" to define the literals with implicit keys: +```TS +let boom = 123; +let bam = [4,5,6]; +let jsonObj = { boom, bam }; +``` + Every value within a `Json` array or object also has a type of `Json`. ##### 1.1.4.2 JSON objects @@ -1394,6 +1401,17 @@ Structs can inherit from multiple other structs. > }; > ``` +A struct literal initialization may use "punning" syntax to initialize fields using variables of the same names: +> ```TS +> struct MyData { +> someNum: num; +> someStr: str; +> } +> let someNum = 1; +> let someStr = "string cheese"; +> let myData = MyData {someNum, someStr}; +> ``` + [`▲ top`][top] --- diff --git a/examples/tests/invalid/json.test.w b/examples/tests/invalid/json.test.w index 3e182276e62..f3d610fcb69 100644 --- a/examples/tests/invalid/json.test.w +++ b/examples/tests/invalid/json.test.w @@ -123,3 +123,15 @@ let objInsteadOfArray: StructyJson = { } }; +// Unkonwn variable in json object punning +{"x":1, y: 2, unknownVar}; +// Unknown variable in explicitly typed json object punning +Json {"x":1, y: 2, unknownVar}; +// Duplicate field in punned json +let numField = 1; +{numField, numField}; +{numField: 5, numField}; + +// Wrong type when using punning +let bucket = new cloud.Bucket(); +Json { bucket }; diff --git a/examples/tests/invalid/structs.test.w b/examples/tests/invalid/structs.test.w index a4881784575..38282c99300 100644 --- a/examples/tests/invalid/structs.test.w +++ b/examples/tests/invalid/structs.test.w @@ -60,3 +60,12 @@ let inlineInflightStruct = inflight () => { name: str; } }; + + +struct SomeStruct1 { + numField: num; +} +let numField = "hello"; +let noSuchField = 1; +SomeStruct1 { numField }; // Wrong type when using punning +SomeStruct1 { noSuchField }; // Wrong field when using punning diff --git a/examples/tests/valid/json.test.w b/examples/tests/valid/json.test.w index ebc6b400b2f..4b5681c2fbe 100644 --- a/examples/tests/valid/json.test.w +++ b/examples/tests/valid/json.test.w @@ -274,3 +274,20 @@ let hasBucket: HasInnerBucket = { a: new cloud.Bucket() } }; + +let numVar = 1; +let strVar = "s"; +let punnedJson1 = {numVar, strVar}; +assert(punnedJson1["numVar"] == 1); +assert(punnedJson1["strVar"] == "s"); +let punnedMutJson1 = MutJson {numVar}; +punnedMutJson1.set("numVar", punnedMutJson1["numVar"].asNum() + 1); +assert(punnedMutJson1["numVar"] == 2); + +struct StructToPun { + numVar: num; + strVar: str; +} +let structToPunFromJson: StructToPun = Json {numVar, strVar}; +assert(structToPunFromJson.numVar == 1); +assert(structToPunFromJson.strVar == "s"); \ No newline at end of file diff --git a/examples/tests/valid/structs.test.w b/examples/tests/valid/structs.test.w index 8c6a91b1010..21e2fcff04b 100644 --- a/examples/tests/valid/structs.test.w +++ b/examples/tests/valid/structs.test.w @@ -110,3 +110,30 @@ struct DocumentedStruct { /// blah blah blah field: str; } + +struct SomeStruct1 { + numField: num; +} +struct SomeStruct2 { + structField: SomeStruct1; + strField: str; +} +struct SomeStruct3 extends SomeStruct2 { + boolField: bool; + otherField: str; +} +let numField = 1337; +let strField = "leet"; +let boolField = true; +let structField = SomeStruct1 { numField }; +// Struct literal initialization with punning +let someStruct3 = SomeStruct3 { + boolField, + strField, + otherField: "good", + structField +}; +assert(someStruct3.boolField == true); +assert(someStruct3.strField == "leet"); +assert(someStruct3.structField.numField == 1337); +assert(someStruct3.otherField == "good"); \ No newline at end of file diff --git a/libs/tree-sitter-wing/grammar.js b/libs/tree-sitter-wing/grammar.js index 60acdc2b367..dac4dfd9743 100644 --- a/libs/tree-sitter-wing/grammar.js +++ b/libs/tree-sitter-wing/grammar.js @@ -735,7 +735,7 @@ module.exports = grammar({ ), map_literal_member: ($) => seq($.expression, "=>", $.expression), - struct_literal_member: ($) => seq($.identifier, ":", $.expression), + struct_literal_member: ($) => choice($.identifier, seq($.identifier, ":", $.expression)), structured_access_expression: ($) => prec.right( PREC.STRUCTURED_ACCESS, @@ -754,8 +754,7 @@ module.exports = grammar({ json_map_literal: ($) => braced(commaSep(field("member", $.json_literal_member))), json_literal_member: ($) => - seq(choice($.identifier, $.string), ":", $.expression), - + choice($.identifier, seq(choice($.identifier, $.string), ":", $.expression)), json_container_type: ($) => $._json_types, _json_types: ($) => choice("Json", "MutJson"), diff --git a/libs/tree-sitter-wing/src/grammar.json b/libs/tree-sitter-wing/src/grammar.json index 838e2c6c0a5..2b9f7a61cd9 100644 --- a/libs/tree-sitter-wing/src/grammar.json +++ b/libs/tree-sitter-wing/src/grammar.json @@ -4472,19 +4472,28 @@ ] }, "struct_literal_member": { - "type": "SEQ", + "type": "CHOICE", "members": [ { "type": "SYMBOL", "name": "identifier" }, { - "type": "STRING", - "value": ":" - }, - { - "type": "SYMBOL", - "name": "expression" + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "identifier" + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "SYMBOL", + "name": "expression" + } + ] } ] }, @@ -4614,28 +4623,37 @@ ] }, "json_literal_member": { - "type": "SEQ", + "type": "CHOICE", "members": [ { - "type": "CHOICE", + "type": "SYMBOL", + "name": "identifier" + }, + { + "type": "SEQ", "members": [ { - "type": "SYMBOL", - "name": "identifier" + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "identifier" + }, + { + "type": "SYMBOL", + "name": "string" + } + ] + }, + { + "type": "STRING", + "value": ":" }, { "type": "SYMBOL", - "name": "string" + "name": "expression" } ] - }, - { - "type": "STRING", - "value": ":" - }, - { - "type": "SYMBOL", - "name": "expression" } ] }, diff --git a/libs/wingc/src/parser.rs b/libs/wingc/src/parser.rs index 04b1fb8cbda..d9662107e6f 100644 --- a/libs/wingc/src/parser.rs +++ b/libs/wingc/src/parser.rs @@ -2430,7 +2430,19 @@ impl<'s> Parser<'s> { continue; } let field_name = self.node_symbol(&field.named_child(0).unwrap()); - let field_value = self.build_expression(&field.named_child(1).unwrap(), phase); + let field_value = if let Some(field_expr_node) = field.named_child(1) { + self.build_expression(&field_expr_node, phase) + } else { + if let Ok(field_name) = &field_name { + Ok(Expr::new( + ExprKind::Reference(Reference::Identifier(field_name.clone())), + self.node_span(&field), + )) + } else { + Err(()) + } + }; + // Add fields to our struct literal, if some are missing or aren't part of the type we'll fail on type checking if let (Ok(k), Ok(v)) = (field_name, field_value) { if fields.contains_key(&k) { @@ -2492,11 +2504,15 @@ impl<'s> Parser<'s> { "identifier" => self.node_symbol(&key_node)?, other => panic!("Unexpected map key type {} at {:?}", other, key_node), }; - let value_node = field_node.named_child(1).unwrap(); + let value = if let Some(value_node) = field_node.named_child(1) { + self.build_expression(&value_node, phase)? + } else { + Expr::new(ExprKind::Reference(Reference::Identifier(key.clone())), key.span()) + }; if fields.contains_key(&key) { - self.add_error(format!("Duplicate key {} in map literal", key), &key_node); + self.add_error(format!("Duplicate key {} in json object literal", key), &key_node); } else { - fields.insert(key, self.build_expression(&value_node, phase)?); + fields.insert(key, value); } } Ok(fields) diff --git a/libs/wingc/src/valid_json_visitor.rs b/libs/wingc/src/valid_json_visitor.rs index 56dd933e250..8778a478bbf 100644 --- a/libs/wingc/src/valid_json_visitor.rs +++ b/libs/wingc/src/valid_json_visitor.rs @@ -1,7 +1,7 @@ use crate::{ ast::{Expr, ExprKind, Intrinsic, IntrinsicKind, Scope}, diagnostic::{report_diagnostic, Diagnostic}, - type_check::{JsonData, JsonDataKind, Type, Types}, + type_check::{JsonData, JsonDataKind, SpannedTypeInfo, Type, Types}, visit::{self, Visit}, }; @@ -22,6 +22,19 @@ impl<'a> ValidJsonVisitor<'a> { pub fn check(&mut self, scope: &Scope) { self.visit_scope(scope); } + + fn report_invalid_json_value(&mut self, inner: &SpannedTypeInfo) { + let tt = self.types.maybe_unwrap_inference(inner.type_); + // Report an error if this isn't a valid type to put in a json (avoiding cascading errors resulting from unresolved types) + if !tt.is_json_legal_value() && !tt.is_unresolved() { + report_diagnostic(Diagnostic { + message: format!("\"{tt}\" is not a legal JSON value"), + span: Some(inner.span.clone()), + annotations: vec![], + hints: vec![], + }) + } + } } impl<'a> Visit<'_> for ValidJsonVisitor<'a> { @@ -70,40 +83,16 @@ impl<'a> Visit<'_> for ValidJsonVisitor<'a> { if !exclude { match kind { JsonDataKind::Type(inner) => { - let tt = self.types.maybe_unwrap_inference(inner.type_); - if !tt.is_json_legal_value() { - report_diagnostic(Diagnostic { - message: format!("\"{tt}\" is not a legal JSON value"), - span: Some(inner.span.clone()), - annotations: vec![], - hints: vec![], - }) - } + self.report_invalid_json_value(inner); } JsonDataKind::Fields(fields) => { for (_, inner) in fields { - let tt = self.types.maybe_unwrap_inference(inner.type_); - if !tt.is_json_legal_value() { - report_diagnostic(Diagnostic { - message: format!("\"{tt}\" is not a legal JSON value"), - span: Some(inner.span.clone()), - annotations: vec![], - hints: vec![], - }) - } + self.report_invalid_json_value(inner); } } JsonDataKind::List(list) => { for v in list { - let tt = self.types.maybe_unwrap_inference(v.type_); - if !tt.is_json_legal_value() { - report_diagnostic(Diagnostic { - message: format!("\"{tt}\" is not a legal JSON value"), - span: Some(v.span.clone()), - annotations: vec![], - hints: vec![], - }) - } + self.report_invalid_json_value(v); } } } diff --git a/tools/hangar/__snapshots__/invalid.ts.snap b/tools/hangar/__snapshots__/invalid.ts.snap index 91beb846428..d9674665bb4 100644 --- a/tools/hangar/__snapshots__/invalid.ts.snap +++ b/tools/hangar/__snapshots__/invalid.ts.snap @@ -3011,6 +3011,20 @@ exports[`json.test.w 1`] = ` | ^ +error: Duplicate key numField in json object literal + --> ../../../examples/tests/invalid/json.test.w:132:12 + | +132 | {numField, numField}; + | ^^^^^^^^ + + +error: Duplicate key numField in json object literal + --> ../../../examples/tests/invalid/json.test.w:133:15 + | +133 | {numField: 5, numField}; + | ^^^^^^^^ + + error: Expected type to be "num", but got "str" instead --> ../../../examples/tests/invalid/json.test.w:6:14 | @@ -3132,6 +3146,20 @@ error: Expected type to be "Array", but got "Json" instead | ^^^^^^ +error: Unknown symbol "unknownVar" + --> ../../../examples/tests/invalid/json.test.w:127:15 + | +127 | {"x":1, y: 2, unknownVar}; + | ^^^^^^^^^^ + + +error: Unknown symbol "unknownVar" + --> ../../../examples/tests/invalid/json.test.w:129:20 + | +129 | Json {"x":1, y: 2, unknownVar}; + | ^^^^^^^^^^ + + error: "Array" is not a legal JSON value --> ../../../examples/tests/invalid/json.test.w:23:17 | @@ -3160,6 +3188,13 @@ error: "Bucket" is not a legal JSON value | ^^^^^^^^^^^^^^^^^^ +error: "Bucket" is not a legal JSON value + --> ../../../examples/tests/invalid/json.test.w:137:8 + | +137 | Json { bucket }; + | ^^^^^^ + + Tests 1 failed (1) Snapshots 1 skipped @@ -4743,6 +4778,27 @@ error: Cannot instantiate type "BucketProps" because it is a struct and not a cl | ^^^^^^^^^^^^^^^^^ +error: Expected type to be "num", but got "str" instead + --> ../../../examples/tests/invalid/structs.test.w:70:15 + | +70 | SomeStruct1 { numField }; // Wrong type when using punning + | ^^^^^^^^ + + +error: "numField" is not initialized + --> ../../../examples/tests/invalid/structs.test.w:71:1 + | +71 | SomeStruct1 { noSuchField }; // Wrong field when using punning + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +error: "noSuchField" is not a field of "SomeStruct1" + --> ../../../examples/tests/invalid/structs.test.w:71:1 + | +71 | SomeStruct1 { noSuchField }; // Wrong field when using punning + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + error: struct "PreflightStruct" must be declared at the top-level of a file --> ../../../examples/tests/invalid/structs.test.w:51:10 | diff --git a/tools/hangar/__snapshots__/test_corpus/valid/json.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/json.test.w_compile_tf-aws.md index e262672752b..97e4d34eb85 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/json.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/json.test.w_compile_tf-aws.md @@ -230,6 +230,17 @@ class $Root extends $stdlib.std.Resource { const notJson = ({"foo": "bar", "stuff": [1, 2, 3], "maybe": ({"good": true, "inner_stuff": [({"hi": 1, "base": "base"})]})}); let mutableJson = ({"foo": "bar", "stuff": [1, 2, 3], "maybe": ({"good": true, "inner_stuff": [({"hi": 1, "base": "base"})]})}); const hasBucket = ({"a": ({"a": this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket")})}); + const numVar = 1; + const strVar = "s"; + const punnedJson1 = ({"numVar": numVar, "strVar": strVar}); + $helpers.assert($helpers.eq($helpers.lookup(punnedJson1, "numVar"), 1), "punnedJson1[\"numVar\"] == 1"); + $helpers.assert($helpers.eq($helpers.lookup(punnedJson1, "strVar"), "s"), "punnedJson1[\"strVar\"] == \"s\""); + const punnedMutJson1 = ({"numVar": numVar}); + ((obj, key, value) => { obj[key] = value; })(punnedMutJson1, "numVar", (((arg) => { if (typeof arg !== "number") {throw new Error("unable to parse " + typeof arg + " " + arg + " as a number")}; return JSON.parse(JSON.stringify(arg)) })($helpers.lookup(punnedMutJson1, "numVar")) + 1)); + $helpers.assert($helpers.eq($helpers.lookup(punnedMutJson1, "numVar"), 2), "punnedMutJson1[\"numVar\"] == 2"); + const structToPunFromJson = ({"numVar": numVar, "strVar": strVar}); + $helpers.assert($helpers.eq(structToPunFromJson.numVar, 1), "structToPunFromJson.numVar == 1"); + $helpers.assert($helpers.eq(structToPunFromJson.strVar, "s"), "structToPunFromJson.strVar == \"s\""); } } const $PlatformManager = new $stdlib.platform.PlatformManager({platformPaths: $platforms}); diff --git a/tools/hangar/__snapshots__/test_corpus/valid/structs.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/structs.test.w_compile_tf-aws.md index ba53b64c40d..162436a039d 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/structs.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/structs.test.w_compile_tf-aws.md @@ -149,6 +149,15 @@ class $Root extends $stdlib.std.Resource { const aNode = ({"val": "someval"}); const bNode = ({"val": "otherval", "next": aNode}); (expect.Util.equal(((json, opts) => { return JSON.stringify(json, null, opts?.indent) })(bNode), "{\"val\":\"otherval\",\"next\":{\"val\":\"someval\"\}\}")); + const numField = 1337; + const strField = "leet"; + const boolField = true; + const structField = ({"numField": numField}); + const someStruct3 = ({"boolField": boolField, "strField": strField, "otherField": "good", "structField": structField}); + $helpers.assert($helpers.eq(someStruct3.boolField, true), "someStruct3.boolField == true"); + $helpers.assert($helpers.eq(someStruct3.strField, "leet"), "someStruct3.strField == \"leet\""); + $helpers.assert($helpers.eq(someStruct3.structField.numField, 1337), "someStruct3.structField.numField == 1337"); + $helpers.assert($helpers.eq(someStruct3.otherField, "good"), "someStruct3.otherField == \"good\""); } } const $PlatformManager = new $stdlib.platform.PlatformManager({platformPaths: $platforms});