Skip to content

Commit

Permalink
feat(compiler): support mutually recursive type references (#5683)
Browse files Browse the repository at this point in the history
Fixes #5665 and unblocks a fix for the `github` winglib (winglang/winglibs#72).

With this change, it's possible for interfaces and structs to reference each other:

```js
// mutually referential interfaces
interface IThing1 {
  m2(): IThing2;
}

interface IThing2 {
  m1(): IThing1;
}

// mutually referential structs
struct M1 {
  m2: M2?;
}

struct M2 {
  m1: M1?;
}
```

A side benefit of this is it's possible for interfaces, structs, and enums to be referenced as type annotations before they have been declared, and enums to be referenced as values before they have been declared:

```js
let opt: SomeEnum? = nil;
let three = SomeEnum.THREE;

enum SomeEnum {
  ONE, TWO, THREE
}

let a: IShape? = nil;
interface IShape {}

let b: Order? = nil;
struct Order {}
```

The same treatment has not yet been applied to classes in this PR since it requires more extensive changes to the type checker. For example, as enforced in other languages like TypeScript, it should not be possible to instantiate classes or access static members before the declaration;

```js
let a = new A(); // Error: Class "A" used before its declaration
class A {}
```

Miscellaneous changes:
- Refactored AST data structures in compiler so that there are dedicated `Struct` and `Enum` structs

## 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)
- [ ] 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 Feb 13, 2024
1 parent c4e9312 commit c07f132
Show file tree
Hide file tree
Showing 21 changed files with 645 additions and 424 deletions.
2 changes: 1 addition & 1 deletion examples/tests/valid/bring_jsii.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ test "sayHello" {
}

let jsiiClass = new jsii_fixture.JsiiClass(10);
assert(jsiiClass.applyClosure(5, (x) => { return x * 2; }) == 10);
assert(jsiiClass.applyClosure(5, (x) => { return x * 2; }) == 10);
13 changes: 12 additions & 1 deletion examples/tests/valid/enums.test.w
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// enums can be referenced by other types before they are defined
struct A {
e: SomeEnum;
}

// type annotations can reference enums before they are defined
let opt: SomeEnum? = nil;

// values can reference enums before they are defined
let three = SomeEnum.THREE;

enum SomeEnum {
ONE, TWO, THREE
}
Expand All @@ -8,4 +19,4 @@ let two: SomeEnum = SomeEnum.TWO; // Try with explicit type annotation
test "inflight" {
assert(one == SomeEnum.ONE);
assert(two == SomeEnum.TWO);
}
}
27 changes: 27 additions & 0 deletions examples/tests/valid/interface.test.w
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
struct A {
// other types can reference interfaces before they are defined
field0: IShape;
}

// type annotations can reference interfaces before they are defined
let a: IShape? = nil;

interface IShape {
// method with a return type
method1(): str;
Expand All @@ -14,3 +22,22 @@ interface IPointy {
interface ISquare extends IShape, IPointy {

}

class C {}

// interfaces can reference classes
interface IClass {
method1(): C;
// method2(): D; // TODO: not supported yet - classes are not hoisted
}

class D {}

// mutually referential interfaces
interface IThing1 {
m2(): IThing2?;
}

interface IThing2 {
m1(): IThing1?;
}
19 changes: 18 additions & 1 deletion examples/tests/valid/structs.test.w
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
interface IFoo {
// other types can reference structs before they are defined
field0(): A;
}

// type annotations can reference structs before they are defined
let a: A? = nil;

struct A {
field0: str;
}
Expand Down Expand Up @@ -70,4 +78,13 @@ test "struct definitions are phase independant" {
};

assert(s2.a == "foo");
}
}

// mutually referential structs
struct M1 {
m2: M2?;
}

struct M2 {
m1: M1?;
}
28 changes: 17 additions & 11 deletions libs/wingc/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,21 @@ pub struct Interface {
pub access: AccessModifier,
}

#[derive(Debug)]
pub struct Struct {
pub name: Symbol,
pub extends: Vec<UserDefinedType>,
pub fields: Vec<StructField>,
pub access: AccessModifier,
}

#[derive(Debug)]
pub struct Enum {
pub name: Symbol,
pub values: IndexSet<Symbol>,
pub access: AccessModifier,
}

#[derive(Debug)]
pub enum BringSource {
BuiltinModule(Symbol),
Expand Down Expand Up @@ -507,17 +522,8 @@ pub enum StmtKind {
Scope(Scope),
Class(Class),
Interface(Interface),
Struct {
name: Symbol,
extends: Vec<UserDefinedType>,
fields: Vec<StructField>,
access: AccessModifier,
},
Enum {
name: Symbol,
values: IndexSet<Symbol>,
access: AccessModifier,
},
Struct(Struct),
Enum(Enum),
TryCatch {
try_statements: Scope,
catch_block: Option<CatchBlock>,
Expand Down
27 changes: 9 additions & 18 deletions libs/wingc/src/dtsify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,23 +251,18 @@ impl<'a> DTSifier<'a> {
code.line(self.dtsify_interface(interface, false));
code.line(self.dtsify_interface(interface, true));
}
StmtKind::Struct {
name,
extends,
fields,
access: _,
} => {
if !extends.is_empty() {
StmtKind::Struct(st) => {
if !st.extends.is_empty() {
code.open(format!(
"export interface {} extends {} {{",
name.name,
extends.iter().map(|udt| udt.to_string()).join(", ")
st.name.name,
st.extends.iter().map(|udt| udt.to_string()).join(", ")
));
} else {
code.open(format!("export interface {} {{", name.name));
code.open(format!("export interface {} {{", st.name.name));
}

for field in fields {
for field in &st.fields {
code.line(format!(
"readonly {}{}: {};",
field.name,
Expand All @@ -281,13 +276,9 @@ impl<'a> DTSifier<'a> {
}
code.close("}");
}
StmtKind::Enum {
name,
values,
access: _,
} => {
code.open(format!("export enum {} {{", name.name));
for value in values {
StmtKind::Enum(enu) => {
code.open(format!("export enum {} {{", enu.name.name));
for value in &enu.values {
code.line(format!("{},", value.name));
}
code.close("}");
Expand Down
59 changes: 39 additions & 20 deletions libs/wingc/src/fold.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::{
ast::{
ArgList, BringSource, CalleeKind, CatchBlock, Class, ClassField, ElifBlock, ElifLetBlock, Elifs, Expr, ExprKind,
FunctionBody, FunctionDefinition, FunctionParameter, FunctionSignature, IfLet, Interface, InterpolatedString,
InterpolatedStringPart, Literal, New, Reference, Scope, Stmt, StmtKind, StructField, Symbol, TypeAnnotation,
TypeAnnotationKind, UserDefinedType,
ArgList, BringSource, CalleeKind, CatchBlock, Class, ClassField, ElifBlock, ElifLetBlock, Elifs, Enum, Expr,
ExprKind, FunctionBody, FunctionDefinition, FunctionParameter, FunctionSignature, IfLet, Interface,
InterpolatedString, InterpolatedStringPart, Literal, New, Reference, Scope, Stmt, StmtKind, Struct, StructField,
Symbol, TypeAnnotation, TypeAnnotationKind, UserDefinedType,
},
dbg_panic,
};
Expand All @@ -24,12 +24,18 @@ pub trait Fold {
fn fold_class_field(&mut self, node: ClassField) -> ClassField {
fold_class_field(self, node)
}
fn fold_struct(&mut self, node: Struct) -> Struct {
fold_struct(self, node)
}
fn fold_struct_field(&mut self, node: StructField) -> StructField {
fold_struct_field(self, node)
}
fn fold_interface(&mut self, node: Interface) -> Interface {
fold_interface(self, node)
}
fn fold_enum(&mut self, node: Enum) -> Enum {
fold_enum(self, node)
}
fn fold_expr(&mut self, node: Expr) -> Expr {
fold_expr(self, node)
}
Expand Down Expand Up @@ -179,22 +185,8 @@ where
StmtKind::Scope(scope) => StmtKind::Scope(f.fold_scope(scope)),
StmtKind::Class(class) => StmtKind::Class(f.fold_class(class)),
StmtKind::Interface(interface) => StmtKind::Interface(f.fold_interface(interface)),
StmtKind::Struct {
name,
extends,
fields,
access,
} => StmtKind::Struct {
name: f.fold_symbol(name),
extends: extends.into_iter().map(|e| f.fold_user_defined_type(e)).collect(),
fields: fields.into_iter().map(|field| f.fold_struct_field(field)).collect(),
access,
},
StmtKind::Enum { name, values, access } => StmtKind::Enum {
name: f.fold_symbol(name),
values: values.into_iter().map(|value| f.fold_symbol(value)).collect(),
access,
},
StmtKind::Struct(st) => StmtKind::Struct(f.fold_struct(st)),
StmtKind::Enum(enu) => StmtKind::Enum(f.fold_enum(enu)),
StmtKind::TryCatch {
try_statements,
catch_block,
Expand Down Expand Up @@ -290,6 +282,33 @@ where
}
}

pub fn fold_struct<F>(f: &mut F, node: Struct) -> Struct
where
F: Fold + ?Sized,
{
Struct {
name: f.fold_symbol(node.name),
extends: node.extends.into_iter().map(|e| f.fold_user_defined_type(e)).collect(),
fields: node
.fields
.into_iter()
.map(|field| f.fold_struct_field(field))
.collect(),
access: node.access,
}
}

pub fn fold_enum<F>(f: &mut F, node: Enum) -> Enum
where
F: Fold + ?Sized,
{
Enum {
name: f.fold_symbol(node.name),
values: node.values.into_iter().map(|v| f.fold_symbol(v)).collect(),
access: node.access,
}
}

pub fn fold_expr<F>(f: &mut F, node: Expr) -> Expr
where
F: Fold + ?Sized,
Expand Down
32 changes: 20 additions & 12 deletions libs/wingc/src/jsify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ use std::{borrow::Borrow, cell::RefCell, cmp::Ordering, collections::BTreeMap, v

use crate::{
ast::{
AccessModifier, ArgList, AssignmentKind, BinaryOperator, BringSource, CalleeKind, Class as AstClass, Elifs, Expr,
ExprKind, FunctionBody, FunctionDefinition, IfLet, InterpolatedStringPart, Literal, New, Phase, Reference, Scope,
Stmt, StmtKind, Symbol, UnaryOperator, UserDefinedType,
AccessModifier, ArgList, AssignmentKind, BinaryOperator, BringSource, CalleeKind, Class as AstClass, Elifs, Enum,
Expr, ExprKind, FunctionBody, FunctionDefinition, IfLet, InterpolatedStringPart, Literal, New, Phase, Reference,
Scope, Stmt, StmtKind, Symbol, UnaryOperator, UserDefinedType,
},
comp_ctx::{CompilationContext, CompilationPhase},
dbg_panic,
Expand Down Expand Up @@ -139,9 +139,12 @@ impl<'a> JSifier<'a> {
jsify_context.visit_ctx.push_env(self.types.get_scope_env(&scope));
for statement in scope.statements.iter().sorted_by(|a, b| match (&a.kind, &b.kind) {
// Put type definitions first so JS won't complain of unknown types
(StmtKind::Class(AstClass { .. }), StmtKind::Class(AstClass { .. })) => Ordering::Equal,
(StmtKind::Class(AstClass { .. }), _) => Ordering::Less,
(_, StmtKind::Class(AstClass { .. })) => Ordering::Greater,
(StmtKind::Enum(_), StmtKind::Enum(_)) => Ordering::Equal,
(StmtKind::Enum(_), _) => Ordering::Less,
(_, StmtKind::Enum(_)) => Ordering::Greater,
(StmtKind::Class(_), StmtKind::Class(_)) => Ordering::Equal,
(StmtKind::Class(_), _) => Ordering::Less,
(_, StmtKind::Class(_)) => Ordering::Greater,
_ => Ordering::Equal,
}) {
let scope_env = self.types.get_scope_env(&scope);
Expand Down Expand Up @@ -1151,10 +1154,15 @@ impl<'a> JSifier<'a> {
StmtKind::Interface { .. } => {
// This is a no-op in JS
}
StmtKind::Struct { .. } => {
StmtKind::Struct(_) => {
// Struct schemas are emitted before jsification phase
}
StmtKind::Enum { name, values, .. } => {
StmtKind::Enum(enu) => {
let Enum {
name,
values,
access: _,
} = enu;
code.open(format!("const {name} ="));
code.add_code(self.jsify_enum(name, values));
code.close(";");
Expand Down Expand Up @@ -1937,10 +1945,10 @@ fn get_public_symbols(scope: &Scope) -> Vec<Symbol> {
StmtKind::Interface(_) => {}
// structs are bringable, but we don't emit anything for them
// unless a static method is called on them
StmtKind::Struct { .. } => {}
StmtKind::Enum { name, access, .. } => {
if *access == AccessModifier::Public {
symbols.push(name.clone());
StmtKind::Struct(_) => {}
StmtKind::Enum(enu) => {
if enu.access == AccessModifier::Public {
symbols.push(enu.name.clone());
}
}
StmtKind::TryCatch { .. } => {}
Expand Down
14 changes: 7 additions & 7 deletions libs/wingc/src/jsify/snapshots/enum_value.snap
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ const $helpers = $stdlib.helpers;
class $Root extends $stdlib.std.Resource {
constructor($scope, $id) {
super($scope, $id);
const MyEnum =
(function (tmp) {
tmp[tmp["B"] = 0] = ",B";
tmp[tmp["C"] = 1] = ",C";
return tmp;
})({})
;
class $Closure1 extends $stdlib.std.AutoIdResource {
_id = $stdlib.core.closureId();
constructor($scope, $id, ) {
Expand Down Expand Up @@ -85,13 +92,6 @@ class $Root extends $stdlib.std.Resource {
});
}
}
const MyEnum =
(function (tmp) {
tmp[tmp["B"] = 0] = ",B";
tmp[tmp["C"] = 1] = ",C";
return tmp;
})({})
;
const x = MyEnum.C;
this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:test", new $Closure1(this, "$Closure1"));
}
Expand Down
8 changes: 4 additions & 4 deletions libs/wingc/src/lsp/document_symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ impl Visit<'_> for DocumentSymbolVisitor {
.document_symbols
.push(create_document_symbol(symbol, SymbolKind::CLASS));
}
StmtKind::Struct { name, .. } => {
let symbol = name;
StmtKind::Struct(st) => {
let symbol = &st.name;
self
.document_symbols
.push(create_document_symbol(symbol, SymbolKind::STRUCT));
}
StmtKind::Enum { name, .. } => {
let symbol = name;
StmtKind::Enum(enu) => {
let symbol = &enu.name;
self
.document_symbols
.push(create_document_symbol(symbol, SymbolKind::ENUM));
Expand Down
6 changes: 3 additions & 3 deletions libs/wingc/src/lsp/symbol_locator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,14 @@ impl<'a> Visit<'a> for SymbolLocator<'a> {

// Handle situations where symbols are actually defined in inner scopes
match &node.kind {
StmtKind::Struct { name, fields, .. } => {
let Some(struct_env) = self.get_env_from_classlike_symbol(name) else {
StmtKind::Struct(st) => {
let Some(struct_env) = self.get_env_from_classlike_symbol(&st.name) else {
return;
};

self.ctx.push_env(struct_env);

for field in fields {
for field in &st.fields {
self.visit_symbol(&field.name);
}

Expand Down
Loading

0 comments on commit c07f132

Please sign in to comment.