Skip to content

Commit

Permalink
feat(compiler): punning (#6752)
Browse files Browse the repository at this point in the history
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)*.
  • Loading branch information
yoav-steinberg authored Jun 21, 2024
1 parent cace7ae commit 56010c4
Show file tree
Hide file tree
Showing 12 changed files with 236 additions and 55 deletions.
18 changes: 18 additions & 0 deletions docs/docs/03-language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
---
Expand Down
12 changes: 12 additions & 0 deletions examples/tests/invalid/json.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
9 changes: 9 additions & 0 deletions examples/tests/invalid/structs.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions examples/tests/valid/json.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -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");
27 changes: 27 additions & 0 deletions examples/tests/valid/structs.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -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");
5 changes: 2 additions & 3 deletions libs/tree-sitter-wing/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"),
Expand Down
58 changes: 38 additions & 20 deletions libs/tree-sitter-wing/src/grammar.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 20 additions & 4 deletions libs/wingc/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 17 additions & 28 deletions libs/wingc/src/valid_json_visitor.rs
Original file line number Diff line number Diff line change
@@ -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},
};

Expand All @@ -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> {
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
Loading

0 comments on commit 56010c4

Please sign in to comment.