From 7e12b93f4f04e2c10c9ed39bce2afed3808c33c7 Mon Sep 17 00:00:00 2001 From: Uri Bar <106860404+staycoolcall911@users.noreply.github.com> Date: Sun, 2 Apr 2023 12:31:41 +0300 Subject: [PATCH] feat(compiler): interface declaration (#1938) This PR adds support for `interface` according to the [spec](https://docs.winglang.io/reference/spec#34-interfaces). Also in this PR: - [x] Removed interface fields from the spec and our codebase ([slack discussion](https://winglang.slack.com/archives/C048QCN2XLJ/p1680077416200329)). - [x] Removed access modifiers from interface methods (to comply with the spec). - [x] (Very) small refactoring in our grammar.js to rename method return type from `type` to `return_type`. - [x] Removed unused parameter `is_class` from SymbolEnv::new Fixes #123 *By submitting this pull request, I confirm that my contribution is made under the terms of the [Monada Contribution License](https://docs.winglang.io/terms-and-policies/contribution-license.html)*. --- docs/04-reference/winglang-spec.md | 10 +- examples/tests/invalid/impl_interface.w | 19 ++ examples/tests/invalid/interface.w | 29 +++ examples/tests/valid/impl_interface.w | 39 ++++ examples/tests/valid/interface.w | 16 ++ libs/tree-sitter-wing/grammar.js | 20 +- .../corpus/statements/class_and_resource.txt | 7 +- libs/wingc/src/ast.rs | 8 + libs/wingc/src/capture.rs | 6 +- libs/wingc/src/jsify.rs | 4 + libs/wingc/src/lib.rs | 2 +- libs/wingc/src/parser.rs | 83 ++++++++- libs/wingc/src/type_check.rs | 174 ++++++++++++++---- libs/wingc/src/type_check/jsii_importer.rs | 20 +- libs/wingc/src/type_check/symbol_env.rs | 1 - libs/wingc/src/visit.rs | 47 ++++- tools/hangar/__snapshots__/invalid.ts.snap | 57 +++++- .../valid/impl_interface.w.ts.snap | 66 +++++++ .../test_corpus/valid/interface.w.ts.snap | 53 ++++++ 19 files changed, 583 insertions(+), 78 deletions(-) create mode 100644 examples/tests/invalid/interface.w create mode 100644 examples/tests/valid/interface.w create mode 100644 tools/hangar/__snapshots__/test_corpus/valid/interface.w.ts.snap diff --git a/docs/04-reference/winglang-spec.md b/docs/04-reference/winglang-spec.md index c10a098552..801bcebe1c 100644 --- a/docs/04-reference/winglang-spec.md +++ b/docs/04-reference/winglang-spec.md @@ -1647,22 +1647,24 @@ separated with commas. All methods of an interface are implicitly public and cannot be of any other type of visibility (private, protected, etc.). +Interface fields are not supported. + > ```TS > // Wing program: > interface IMyInterface1 { -> field1: num; > method1(x: num): str; > }; > interface IMyInterface2 { -> inflight field2: str; > inflight method2(): str; > }; > resource MyResource impl IMyInterface1, IMyInterface2 { -> inflight field2: str; +> field1: num; +> field2: str; +> > inflight init(x: num) { > // inflight client initialization -> this.field2 = "sample"; > this.field1 = x; +> this.field2 = "sample"; > } > method1(x: num): str { > return "sample: ${x}"; diff --git a/examples/tests/invalid/impl_interface.w b/examples/tests/invalid/impl_interface.w index f58113509f..cd5b873230 100644 --- a/examples/tests/invalid/impl_interface.w +++ b/examples/tests/invalid/impl_interface.w @@ -17,3 +17,22 @@ resource C impl cloud.Bucket { // ^^^^^^^^^^^ Error: cloud.Bucket is a resource, not an interface init() {} } + +interface I1 { + method_1(x: num): num; +} + +interface I2 extends I1 { + inflight method_2(x: str): str; +} + +interface I3 extends I2 { + method_3(x: Array): Array; +} + +resource r impl I3 { + // ^ Resource "r" does not implement method "method_1" of interface "I3" + // ^ Resource "r" does not implement method "method_2" of interface "I3" + // ^ Resource "r" does not implement method "method_3" of interface "I3" + init() {} +} \ No newline at end of file diff --git a/examples/tests/invalid/interface.w b/examples/tests/invalid/interface.w new file mode 100644 index 0000000000..2e259ea840 --- /dev/null +++ b/examples/tests/invalid/interface.w @@ -0,0 +1,29 @@ +// interface extend loop +interface IA extends IB { + // ^^ Unknown symbol "IB" +} + +interface IB extends IA { +} + +// interface extends interface which doesn't exist +interface IExist extends IDontExist { + // ^^^^^^^^^^ Unknown symbol "IDontExist" +} + +// interface extends class +class ISomeClass { + init(){} +} +interface ISomeInterface extends ISomeClass { + // Interface "ISomeInterface (at ../../examples/tests/invalid/interface.w:21:11)" extends "ISomeClass", which is not an interface +} + +// interface with multiple methods having the same name, different signature +interface IWithSameName { + foo(); + foo(); + // ^^^ Symbol "foo" already defined in this scope + foo(): num; + // ^^^ Symbol "foo" already defined in this scope +} \ No newline at end of file diff --git a/examples/tests/valid/impl_interface.w b/examples/tests/valid/impl_interface.w index 50c5e21224..5f1a393674 100644 --- a/examples/tests/valid/impl_interface.w +++ b/examples/tests/valid/impl_interface.w @@ -12,3 +12,42 @@ let x: cloud.IQueueOnMessageHandler = new A(); let y = inflight () => { x.handle("hello world!"); }; + +interface I1 { + method_1(x: num): num; +} + +interface I2 extends I1 { + inflight method_2(x: str): str; +} + +interface I3 extends I2 { + method_3(x: Array): Array; +} + +resource r impl I3 { + init() {} + method_1(x: num): num { + return x; + } + inflight method_2(x: str): str { + return x; + } + method_3(x: Array): Array { + return x; + } +} + +// a variable of some interface type can be assigned a class instance that implements it. +interface IAnimal { + inflight eat(); +} + +resource Dog impl IAnimal { + init(){} + inflight eat() { + return; + } +} + +let z: IAnimal = new Dog(); \ No newline at end of file diff --git a/examples/tests/valid/interface.w b/examples/tests/valid/interface.w new file mode 100644 index 0000000000..b937eb8cd3 --- /dev/null +++ b/examples/tests/valid/interface.w @@ -0,0 +1,16 @@ +interface IShape { + // method with a return type + method_1(): str; + // method without a return type + method_2(); + // method with a return type of the interface type + method_3(): IShape; +} + +interface IPointy { + method_2(); +} + +interface ISquare extends IShape, IPointy { + +} diff --git a/libs/tree-sitter-wing/grammar.js b/libs/tree-sitter-wing/grammar.js index ceb278ac46..678030df02 100644 --- a/libs/tree-sitter-wing/grammar.js +++ b/libs/tree-sitter-wing/grammar.js @@ -210,7 +210,7 @@ module.exports = grammar({ seq( "interface", field("name", $.identifier), - optional(seq("extends", field("implements", commaSep1($.custom_type)))), + optional(seq("extends", field("extends", commaSep1($.custom_type)))), field("implementation", $.interface_implementation) ), interface_implementation: ($) => @@ -415,14 +415,14 @@ module.exports = grammar({ extern_modifier : ($) => seq("extern", $.string), + _return_type: ($) => $._type_annotation, + method_signature: ($) => seq( - optional(field("access_modifier", $.access_modifier)), - optional(field("static", $.static)), optional(field("async", $.async_modifier)), field("name", $.identifier), field("parameter_list", $.parameter_list), - optional(field("return_type", $._type_annotation)), + optional($._return_type), ";" ), @@ -434,18 +434,16 @@ module.exports = grammar({ optional(field("async", $.async_modifier)), field("name", $.identifier), field("parameter_list", $.parameter_list), - optional(field("return_type", $._type_annotation)), + optional($._return_type), choice(field("block", $.block), ";") ), inflight_method_signature: ($) => seq( - optional(field("access_modifier", $.access_modifier)), - optional(field("static", $.static)), field("phase_modifier", $._inflight_specifier), field("name", $.identifier), field("parameter_list", $.parameter_list), - optional(field("return_type", $._type_annotation)), + optional($._return_type), ";" ), @@ -457,7 +455,7 @@ module.exports = grammar({ field("phase_modifier", $._inflight_specifier), field("name", $.identifier), field("parameter_list", $.parameter_list), - optional(field("return_type", $._type_annotation)), + optional($._return_type), choice(field("block", $.block), ";") ), @@ -557,7 +555,7 @@ module.exports = grammar({ preflight_closure: ($) => seq( field("parameter_list", $.parameter_list), - optional(field("return_type", $._type_annotation)), + optional($._return_type), "=>", field("block", $.block) ), @@ -566,7 +564,7 @@ module.exports = grammar({ seq( "inflight", field("parameter_list", $.parameter_list), - optional(field("return_type", $._type_annotation)), + optional($._return_type), "=>", field("block", $.block) ), diff --git a/libs/tree-sitter-wing/test/corpus/statements/class_and_resource.txt b/libs/tree-sitter-wing/test/corpus/statements/class_and_resource.txt index 61fe6927e6..49985d7fc1 100644 --- a/libs/tree-sitter-wing/test/corpus/statements/class_and_resource.txt +++ b/libs/tree-sitter-wing/test/corpus/statements/class_and_resource.txt @@ -82,7 +82,7 @@ resource A { async preflight_func() {} public preflight_func2() {} inflight inflight_func() {} - public inflight inflight_func2() {} + public inflight inflight_func2(): num {} pf_member: str; inflight if_member: str; public inflight var if_member2: str; @@ -124,6 +124,7 @@ resource A { access_modifier: (access_modifier) name: (identifier) parameter_list: (parameter_list) + type: (builtin_type) block: (block) ) (class_field @@ -193,9 +194,9 @@ interface A extends B, C { (source (interface_definition name: (identifier) - implements: (custom_type + extends: (custom_type object: (identifier)) - implements: (custom_type + extends: (custom_type object: (identifier)) implementation: (interface_implementation (method_signature diff --git a/libs/wingc/src/ast.rs b/libs/wingc/src/ast.rs index a3a9f1473b..2aef369db8 100644 --- a/libs/wingc/src/ast.rs +++ b/libs/wingc/src/ast.rs @@ -265,6 +265,13 @@ pub struct Class { pub is_resource: bool, } +#[derive(Debug)] +pub struct Interface { + pub name: Symbol, + pub methods: Vec<(Symbol, FunctionSignature)>, + pub extends: Vec, +} + #[derive(Debug)] pub enum StmtKind { Bring { @@ -302,6 +309,7 @@ pub enum StmtKind { Return(Option), Scope(Scope), Class(Class), + Interface(Interface), Struct { name: Symbol, extends: Vec, diff --git a/libs/wingc/src/capture.rs b/libs/wingc/src/capture.rs index bde6d9b894..b1c212fe9c 100644 --- a/libs/wingc/src/capture.rs +++ b/libs/wingc/src/capture.rs @@ -399,7 +399,11 @@ fn scan_captures_in_inflight_scope(scope: &Scope, diagnostics: &mut Diagnostics) todo!() } // Type definitions with no expressions in them can't capture anything - StmtKind::Struct { .. } | StmtKind::Enum { .. } | StmtKind::Break | StmtKind::Continue => {} + StmtKind::Struct { .. } + | StmtKind::Interface { .. } + | StmtKind::Enum { .. } + | StmtKind::Break + | StmtKind::Continue => {} StmtKind::TryCatch { try_statements, catch_block, diff --git a/libs/wingc/src/jsify.rs b/libs/wingc/src/jsify.rs index 8a460c0ae5..c992077414 100644 --- a/libs/wingc/src/jsify.rs +++ b/libs/wingc/src/jsify.rs @@ -642,6 +642,10 @@ impl<'a> JSifier<'a> { } } StmtKind::Class(class) => self.jsify_class(env, class, context), + StmtKind::Interface { .. } => { + // This is a no-op in JS + format!("") + } StmtKind::Struct { .. } => { // This is a no-op in JS format!("") diff --git a/libs/wingc/src/lib.rs b/libs/wingc/src/lib.rs index cc4dfe034f..dcc52778bd 100644 --- a/libs/wingc/src/lib.rs +++ b/libs/wingc/src/lib.rs @@ -165,7 +165,7 @@ pub fn parse(source_path: &Path) -> (Scope, Diagnostics) { } pub fn type_check(scope: &mut Scope, types: &mut Types, source_path: &Path) -> Diagnostics { - let env = SymbolEnv::new(None, types.void(), false, false, Phase::Preflight, 0); + let env = SymbolEnv::new(None, types.void(), false, Phase::Preflight, 0); scope.set_env(env); // note: Globals are emitted here and wrapped in "{ ... }" blocks. Wrapping makes these emissions, actual diff --git a/libs/wingc/src/parser.rs b/libs/wingc/src/parser.rs index 634f054d85..c1c9270cd7 100644 --- a/libs/wingc/src/parser.rs +++ b/libs/wingc/src/parser.rs @@ -8,8 +8,8 @@ use tree_sitter_traversal::{traverse, Order}; use crate::ast::{ ArgList, BinaryOperator, CatchBlock, Class, ClassField, Constructor, ElifBlock, Expr, ExprKind, FunctionBody, - FunctionDefinition, FunctionSignature, InterpolatedString, InterpolatedStringPart, Literal, Phase, Reference, Scope, - Stmt, StmtKind, Symbol, TypeAnnotation, UnaryOperator, UserDefinedType, + FunctionDefinition, FunctionSignature, Interface, InterpolatedString, InterpolatedStringPart, Literal, Phase, + Reference, Scope, Stmt, StmtKind, Symbol, TypeAnnotation, UnaryOperator, UserDefinedType, }; use crate::diagnostic::{Diagnostic, DiagnosticLevel, DiagnosticResult, Diagnostics, WingSpan}; use crate::WINGSDK_STD_MODULE; @@ -26,7 +26,6 @@ pub struct Parser<'a> { // this is meant to serve as a bandaide to be removed once wing is further developed. // k=grammar, v=optional_message, example: ("generic", "targed impl: 1.0.0") static UNIMPLEMENTED_GRAMMARS: phf::Map<&'static str, &'static str> = phf_map! { - "interface_definition" => "see https://github.com/winglang/wing/issues/123", "any" => "see https://github.com/winglang/wing/issues/434", "void" => "see https://github.com/winglang/wing/issues/432", "nil" => "see https://github.com/winglang/wing/issues/433", @@ -193,6 +192,7 @@ impl<'s> Parser<'s> { "return_statement" => self.build_return_statement(statement_node)?, "class_definition" => self.build_class_statement(statement_node, false)?, "resource_definition" => self.build_class_statement(statement_node, true)?, + "interface_definition" => self.build_interface_statement(statement_node)?, "enum_definition" => self.build_enum_statement(statement_node)?, "try_catch_statement" => self.build_try_catch_statement(statement_node)?, "struct_definition" => self.build_struct_definition_statement(statement_node)?, @@ -584,6 +584,83 @@ impl<'s> Parser<'s> { })) } + fn build_interface_statement(&self, statement_node: &Node) -> DiagnosticResult { + let mut cursor = statement_node.walk(); + let mut extends = vec![]; + let mut methods = vec![]; + let name = self.node_symbol(&statement_node.child_by_field_name("name").unwrap())?; + + for interface_element in statement_node + .child_by_field_name("implementation") + .unwrap() + .named_children(&mut cursor) + { + if interface_element.is_extra() { + continue; + } + match interface_element.kind() { + "method_signature" => { + let method_name = self.node_symbol(&interface_element.child_by_field_name("name").unwrap()); + let func_sig = self.build_function_signature(&interface_element, Phase::Preflight); + match (method_name, func_sig) { + (Ok(method_name), Ok(func_sig)) => methods.push((method_name, func_sig)), + _ => {} + } + } + "inflight_method_signature" => { + let method_name = self.node_symbol(&interface_element.child_by_field_name("name").unwrap()); + let func_sig = self.build_function_signature(&interface_element, Phase::Inflight); + match (method_name, func_sig) { + (Ok(method_name), Ok(func_sig)) => methods.push((method_name, func_sig)), + _ => {} + } + } + "ERROR" => { + self + .add_error::(format!("Expected interface element node"), &interface_element) + .err(); + } + other => { + panic!( + "Unexpected interface element node type {} || {:#?}", + other, interface_element + ); + } + } + } + + for extend in statement_node.children_by_field_name("extends", &mut cursor) { + // ignore comments + if extend.is_extra() { + continue; + } + + // ignore commas + if !extend.is_named() { + continue; + } + + if let Ok(TypeAnnotation::UserDefined(interface_type)) = self.build_udt_annotation(&extend) { + extends.push(interface_type); + } + } + + Ok(StmtKind::Interface(Interface { name, methods, extends })) + } + + fn build_function_signature(&self, func_sig_node: &Node, phase: Phase) -> DiagnosticResult { + let parameters = self.build_parameter_list(&func_sig_node.child_by_field_name("parameter_list").unwrap())?; + Ok(FunctionSignature { + parameters: parameters.iter().map(|p| p.1.clone()).collect(), + return_type: if let Some(rt) = func_sig_node.child_by_field_name("type") { + Some(Box::new(self.build_type_annotation(&rt)?)) + } else { + None + }, + phase, + }) + } + fn build_anonymous_closure(&self, anon_closure_node: &Node, phase: Phase) -> DiagnosticResult { self.build_function_definition(anon_closure_node, phase) } diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs index 1345db8cd0..567a0c1e74 100644 --- a/libs/wingc/src/type_check.rs +++ b/libs/wingc/src/type_check.rs @@ -1,8 +1,9 @@ mod jsii_importer; pub mod symbol_env; use crate::ast::{ - ArgList, BinaryOperator, Class as AstClass, Expr, ExprKind, FunctionBody, InterpolatedStringPart, Literal, Phase, - Reference, Scope, Stmt, StmtKind, Symbol, ToSpan, TypeAnnotation, UnaryOperator, UserDefinedType, + ArgList, BinaryOperator, Class as AstClass, Expr, ExprKind, FunctionBody, Interface as AstInterface, + InterpolatedStringPart, Literal, Phase, Reference, Scope, Stmt, StmtKind, Symbol, ToSpan, TypeAnnotation, + UnaryOperator, UserDefinedType, }; use crate::diagnostic::{Diagnostic, DiagnosticLevel, Diagnostics, TypeError}; use crate::{ @@ -696,6 +697,13 @@ impl TypeRef { None } } + fn as_mut_interface(&mut self) -> Option<&mut Interface> { + if let Type::Interface(ref mut iface) = **self { + Some(iface) + } else { + None + } + } fn maybe_unwrap_option(&self) -> TypeRef { if let Type::Optional(ref t) = **self { @@ -880,7 +888,7 @@ impl Types { // TODO: this is hack to create the top-level mapping from lib names to symbols // We construct a void ref by hand since we can't call self.void() while constructing the Types struct let void_ref = UnsafeRef::(&*types[void_idx] as *const Type); - let libraries = SymbolEnv::new(None, void_ref, false, false, Phase::Preflight, 0); + let libraries = SymbolEnv::new(None, void_ref, false, Phase::Preflight, 0); Self { types, @@ -1539,7 +1547,6 @@ impl<'a> TypeChecker<'a> { Some(env.get_ref()), sig.return_type, false, - false, func_def.signature.phase, statement_idx, ); @@ -1863,7 +1870,7 @@ impl<'a> TypeChecker<'a> { } }; - let mut scope_env = SymbolEnv::new(Some(env.get_ref()), env.return_type, false, false, env.phase, stmt.idx); + let mut scope_env = SymbolEnv::new(Some(env.get_ref()), env.return_type, false, env.phase, stmt.idx); match scope_env.define( &iterator, SymbolKind::make_variable(iterator_type, false, true, env.phase), @@ -1886,7 +1893,6 @@ impl<'a> TypeChecker<'a> { Some(env.get_ref()), env.return_type, false, - false, env.phase, stmt.idx, )); @@ -1907,7 +1913,6 @@ impl<'a> TypeChecker<'a> { Some(env.get_ref()), env.return_type, false, - false, env.phase, stmt.idx, )); @@ -1921,7 +1926,6 @@ impl<'a> TypeChecker<'a> { Some(env.get_ref()), env.return_type, false, - false, env.phase, stmt.idx, )); @@ -1933,7 +1937,6 @@ impl<'a> TypeChecker<'a> { Some(env.get_ref()), env.return_type, false, - false, env.phase, stmt.idx, )); @@ -2013,7 +2016,6 @@ impl<'a> TypeChecker<'a> { Some(env.get_ref()), env.return_type, false, - false, env.phase, stmt.idx, )); @@ -2077,7 +2079,7 @@ impl<'a> TypeChecker<'a> { }; // Create environment representing this class, for now it'll be empty just so we can support referencing ourselves from the class definition. - let dummy_env = SymbolEnv::new(None, self.types.void(), true, false, env.phase, stmt.idx); + let dummy_env = SymbolEnv::new(None, self.types.void(), false, env.phase, stmt.idx); let impl_interfaces = implements .iter() @@ -2086,7 +2088,7 @@ impl<'a> TypeChecker<'a> { if t.as_interface().is_some() { Some(t) } else { - self.general_type_error(format!("Class {}'s implements \"{}\" is not an interface", name, t)); + self.general_type_error(format!("Expected an interface, instead found type \"{}\"", t)); None } }) @@ -2116,7 +2118,7 @@ impl<'a> TypeChecker<'a> { }; // Create a the real class environment to be filled with the class AST types - let mut class_env = SymbolEnv::new(parent_class_env, self.types.void(), true, false, env.phase, stmt.idx); + let mut class_env = SymbolEnv::new(parent_class_env, self.types.void(), false, env.phase, stmt.idx); // Add fields to the class env for field in fields.iter() { @@ -2197,7 +2199,6 @@ impl<'a> TypeChecker<'a> { let mut constructor_env = SymbolEnv::new( Some(env.get_ref()), constructor_sig.return_type, - false, true, constructor.signature.phase, stmt.idx, @@ -2239,7 +2240,6 @@ impl<'a> TypeChecker<'a> { Some(env.get_ref()), method_sig.return_type, false, - false, method_sig.phase, stmt.idx, ); @@ -2307,13 +2307,81 @@ impl<'a> TypeChecker<'a> { } } } + StmtKind::Interface(AstInterface { name, methods, extends }) => { + // Create environment representing this interface, for now it'll be empty just so we can support referencing ourselves from the interface definition. + let dummy_env = SymbolEnv::new(None, self.types.void(), false, env.phase, stmt.idx); + + let extend_interfaces = extends + .iter() + .filter_map(|i| { + let t = resolve_user_defined_type(i, env, stmt.idx).unwrap_or_else(|e| self.type_error(e)); + if t.as_interface().is_some() { + Some(t) + } else { + // The type checker resolves non-existing definitions to `any`, so we avoid duplicate errors by checking for that here + if !t.is_anything() { + self.general_type_error(format!("Expected an interface, instead found type \"{}\"", t)); + } + None + } + }) + .collect::>(); + + // Create the interface type and add it to the current environment (so interface implementation can reference itself) + let interface_spec = Interface { + name: name.clone(), + env: dummy_env, + extends: extend_interfaces.clone(), + }; + let mut interface_type = self.types.add_type(Type::Interface(interface_spec)); + match env.define(name, SymbolKind::Type(interface_type), StatementIdx::Top) { + Err(type_error) => { + self.type_error(type_error); + } + _ => {} + }; + + // Create the real interface environment to be filled with the interface AST types + let mut interface_env = SymbolEnv::new(None, self.types.void(), false, env.phase, stmt.idx); + + // Add methods to the interface env + for (method_name, sig) in methods.iter() { + let mut method_type = + self.resolve_type_annotation(&TypeAnnotation::FunctionSignature(sig.clone()), env, stmt.idx); + // use the interface type as the function's "this" type + if let Type::Function(ref mut f) = *method_type { + f.this_type = Some(interface_type.clone()); + } else { + panic!("Expected method type to be a function"); + } + + match interface_env.define( + method_name, + SymbolKind::make_variable(method_type, false, false, sig.phase), + StatementIdx::Top, + ) { + Err(type_error) => { + self.type_error(type_error); + } + _ => {} + }; + } + + // add methods from all extended interfaces to the interface env + if let Err(e) = add_parent_members_to_iface_env(&extend_interfaces, name, &mut interface_env) { + self.type_error(e); + } + + // Replace the dummy interface environment with the real one before type checking the methods + interface_type.as_mut_interface().unwrap().env = interface_env; + } StmtKind::Struct { name, extends, members } => { // Note: structs don't have a parent environment, instead they flatten their parent's members into the struct's env. // If we encounter an existing member with the same name and type we skip it, if the types are different we // fail type checking. // Create an environment for the struct - let mut struct_env = SymbolEnv::new(None, self.types.void(), true, false, env.phase, stmt.idx); + let mut struct_env = SymbolEnv::new(None, self.types.void(), false, env.phase, stmt.idx); // Add fields to the struct env for field in members.iter() { @@ -2394,13 +2462,13 @@ impl<'a> TypeChecker<'a> { finally_statements, } => { // Create a new environment for the try block - let try_env = SymbolEnv::new(Some(env.get_ref()), env.return_type, false, false, env.phase, stmt.idx); + let try_env = SymbolEnv::new(Some(env.get_ref()), env.return_type, false, env.phase, stmt.idx); try_statements.set_env(try_env); self.inner_scopes.push(try_statements); // Create a new environment for the catch block if let Some(catch_block) = catch_block { - let mut catch_env = SymbolEnv::new(Some(env.get_ref()), env.return_type, false, false, env.phase, stmt.idx); + let mut catch_env = SymbolEnv::new(Some(env.get_ref()), env.return_type, false, env.phase, stmt.idx); // Add the exception variable to the catch block if let Some(exception_var) = &catch_block.exception_var { @@ -2421,7 +2489,7 @@ impl<'a> TypeChecker<'a> { // Create a new environment for the finally block if let Some(finally_statements) = finally_statements { - let finally_env = SymbolEnv::new(Some(env.get_ref()), env.return_type, false, false, env.phase, stmt.idx); + let finally_env = SymbolEnv::new(Some(env.get_ref()), env.return_type, false, env.phase, stmt.idx); finally_statements.set_env(finally_env); self.inner_scopes.push(finally_statements); } @@ -2497,7 +2565,7 @@ impl<'a> TypeChecker<'a> { /// /// #Arguments /// - /// * `args` - List of aruments to add, each element is a tuple of the arugment symbol and whether it's + /// * `args` - List of arguments to add, each element is a tuple of the argument symbol and whether it's /// reassignable arg or not. Note that the argument types are figured out from `sig`. /// * `sig` - The function signature (used to figure out the type of each argument). /// * `env` - The function's environment to prime with the args. @@ -2561,14 +2629,7 @@ impl<'a> TypeChecker<'a> { types_map.insert(format!("{o}"), (*o, *n)); } - let new_env = SymbolEnv::new( - None, - original_type_class.env.return_type, - true, - false, - Phase::Independent, - 0, - ); + let new_env = SymbolEnv::new(None, original_type_class.env.return_type, false, Phase::Independent, 0); let tt = Type::Class(Class { name: original_type_class.name.clone(), env: new_env, @@ -3022,9 +3083,6 @@ fn add_parent_members_to_struct_env( .expect("Expected struct member to be a variable") .type_; if let Some(existing_type) = struct_env.try_lookup(&parent_member_name, None) { - // We compare types in both directions to make sure they are exactly the same type and not inheriting from each other - // TODO: does this make sense? We should add an `is_a()` methdod to `Type` to check if a type is a subtype and use that - // when we want to check for subtypes and use equality for strict comparisons. let existing_type = existing_type .as_variable() .expect("Expected struct member to be a variable") @@ -3053,6 +3111,60 @@ fn add_parent_members_to_struct_env( Ok(()) } +// TODO: dup code with `add_parent_members_to_struct_env` +fn add_parent_members_to_iface_env( + extends_types: &Vec, + name: &Symbol, + iface_env: &mut SymbolEnv, +) -> Result<(), TypeError> { + // Add members of all parents to the interface's environment + for parent_type in extends_types.iter() { + let parent_iface = if let Some(parent_iface) = parent_type.as_interface() { + parent_iface + } else { + return Err(TypeError { + message: format!( + "Type \"{}\" extends \"{}\" which should be an interface", + name.name, parent_type + ), + span: name.span.clone(), + }); + }; + // Add each member of current parent to the interface's environment (if it wasn't already added by a previous parent) + for (parent_member_name, parent_member, _) in parent_iface.env.iter(true) { + let member_type = parent_member + .as_variable() + .expect("Expected interface member to be a variable") + .type_; + if let Some(existing_type) = iface_env.try_lookup(&parent_member_name, None) { + let existing_type = existing_type + .as_variable() + .expect("Expected interface member to be a variable") + .type_; + if !existing_type.is_same_type_as(&member_type) { + return Err(TypeError { + span: name.span.clone(), + message: format!( + "Interface \"{}\" extends \"{}\" but has a conflicting member \"{}\" ({} != {})", + name, parent_type, parent_member_name, member_type, member_type + ), + }); + } + } else { + iface_env.define( + &Symbol { + name: parent_member_name, + span: name.span.clone(), + }, + SymbolKind::make_variable(member_type, false, true, iface_env.phase), + StatementIdx::Top, + )?; + } + } + } + Ok(()) +} + pub fn resolve_user_defined_type( user_defined_type: &UserDefinedType, env: &SymbolEnv, diff --git a/libs/wingc/src/type_check/jsii_importer.rs b/libs/wingc/src/type_check/jsii_importer.rs index d84a04d7f5..a97e963225 100644 --- a/libs/wingc/src/type_check/jsii_importer.rs +++ b/libs/wingc/src/type_check/jsii_importer.rs @@ -137,6 +137,8 @@ impl<'a> JsiiImporter<'a> { match collection_kind { "array" => self.wing_types.add_type(Type::Array(wing_type)), "map" => self.wing_types.add_type(Type::Map(wing_type)), + // set is intentionally left out, since in JSII “collection + // kind” is only either map or array. _ => panic!("Unsupported collection kind '{}'", collection_kind), } } else if let Some(Value::Object(_)) = obj.get("union") { @@ -230,7 +232,7 @@ impl<'a> JsiiImporter<'a> { } else { let ns = self.wing_types.add_namespace(Namespace { name: type_name.assembly().to_string(), - env: SymbolEnv::new(None, self.wing_types.void(), false, false, self.env.phase, 0), + env: SymbolEnv::new(None, self.wing_types.void(), false, self.env.phase, 0), }); self .wing_types @@ -272,14 +274,7 @@ impl<'a> JsiiImporter<'a> { } else { let ns = self.wing_types.add_namespace(Namespace { name: namespace_name.to_string(), - env: SymbolEnv::new( - Some(parent_ns.env.get_ref()), - self.wing_types.void(), - false, - false, - flight, - 0, - ), + env: SymbolEnv::new(Some(parent_ns.env.get_ref()), self.wing_types.void(), false, flight, 0), }); parent_ns .env @@ -346,7 +341,6 @@ impl<'a> JsiiImporter<'a> { let mut iface_env = SymbolEnv::new( None, self.wing_types.void(), - true, false, self.env.phase, self.import_statement_idx, @@ -359,7 +353,6 @@ impl<'a> JsiiImporter<'a> { env: SymbolEnv::new( None, self.wing_types.void(), - true, false, iface_env.phase, self.import_statement_idx, @@ -371,7 +364,6 @@ impl<'a> JsiiImporter<'a> { env: SymbolEnv::new( None, self.wing_types.void(), - true, false, iface_env.phase, self.import_statement_idx, @@ -622,7 +614,7 @@ impl<'a> JsiiImporter<'a> { }; // Create environment representing this class, for now it'll be empty just so we can support referencing ourselves from the class definition. - let dummy_env = SymbolEnv::new(None, self.wing_types.void(), true, false, phase, 0); + let dummy_env = SymbolEnv::new(None, self.wing_types.void(), false, phase, 0); let new_type_symbol = Self::jsii_name_to_symbol(type_name, &jsii_class.location_in_module); // Create the new resource/class type and add it to the current environment. // When adding the class methods below we'll be able to reference this type. @@ -678,7 +670,7 @@ impl<'a> JsiiImporter<'a> { self.register_jsii_type(&jsii_class_fqn, &new_type_symbol, new_type); // Create class's actual environment before we add properties and methods to it - let mut class_env = SymbolEnv::new(base_class_env, self.wing_types.void(), true, false, phase, 0); + let mut class_env = SymbolEnv::new(base_class_env, self.wing_types.void(), false, phase, 0); // Add constructor to the class environment let jsii_initializer = jsii_class.initializer.as_ref(); diff --git a/libs/wingc/src/type_check/symbol_env.rs b/libs/wingc/src/type_check/symbol_env.rs index 511d0cdc1e..1d4afbded0 100644 --- a/libs/wingc/src/type_check/symbol_env.rs +++ b/libs/wingc/src/type_check/symbol_env.rs @@ -67,7 +67,6 @@ impl SymbolEnv { pub fn new( parent: Option, return_type: TypeRef, - _is_class: bool, is_init: bool, phase: Phase, statement_idx: usize, diff --git a/libs/wingc/src/visit.rs b/libs/wingc/src/visit.rs index 5fb14121b0..74c82757f3 100644 --- a/libs/wingc/src/visit.rs +++ b/libs/wingc/src/visit.rs @@ -1,6 +1,6 @@ use crate::ast::{ - ArgList, Class, Constructor, Expr, ExprKind, FunctionBody, FunctionDefinition, InterpolatedStringPart, Literal, - Reference, Scope, Stmt, StmtKind, Symbol, TypeAnnotation, + ArgList, Class, Constructor, Expr, ExprKind, FunctionBody, FunctionDefinition, FunctionSignature, Interface, + InterpolatedStringPart, Literal, Reference, Scope, Stmt, StmtKind, Symbol, TypeAnnotation, }; /// Visitor pattern inspired by implementation from https://docs.rs/syn/latest/syn/visit/index.html @@ -38,6 +38,9 @@ pub trait Visit<'ast> { fn visit_class(&mut self, node: &'ast Class) { visit_class(self, node); } + fn visit_interface(&mut self, node: &'ast Interface) { + visit_interface(self, node); + } fn visit_constructor(&mut self, node: &'ast Constructor) { visit_constructor(self, node); } @@ -53,6 +56,9 @@ pub trait Visit<'ast> { fn visit_function_definition(&mut self, node: &'ast FunctionDefinition) { visit_function_definition(self, node); } + fn visit_function_signature(&mut self, node: &'ast FunctionSignature) { + visit_function_signature(self, node); + } fn visit_args(&mut self, node: &'ast ArgList) { visit_args(self, node); } @@ -145,6 +151,9 @@ where StmtKind::Class(class) => { v.visit_class(class); } + StmtKind::Interface(interface) => { + v.visit_interface(interface); + } StmtKind::Struct { name, extends, members } => { v.visit_symbol(name); for extend in extends { @@ -199,6 +208,22 @@ where } } +pub fn visit_interface<'ast, V>(v: &mut V, node: &'ast Interface) +where + V: Visit<'ast> + ?Sized, +{ + v.visit_symbol(&node.name); + + for method in &node.methods { + v.visit_symbol(&method.0); + v.visit_function_signature(&method.1); + } + + for extend in &node.extends { + v.visit_symbol(&extend.root); + } +} + pub fn visit_constructor<'ast, V>(v: &mut V, node: &'ast Constructor) where V: Visit<'ast> + ?Sized, @@ -334,19 +359,25 @@ pub fn visit_function_definition<'ast, V>(v: &mut V, node: &'ast FunctionDefinit where V: Visit<'ast> + ?Sized, { + v.visit_function_signature(&node.signature); for param in &node.parameters { v.visit_symbol(¶m.0); } - for param_type in &node.signature.parameters { + if let FunctionBody::Statements(scope) = &node.body { + v.visit_scope(scope); + }; +} + +pub fn visit_function_signature<'ast, V>(v: &mut V, node: &'ast FunctionSignature) +where + V: Visit<'ast> + ?Sized, +{ + for param_type in &node.parameters { v.visit_type_annotation(param_type); } - if let Some(return_type) = &node.signature.return_type { + if let Some(return_type) = &node.return_type { v.visit_type_annotation(return_type); } - - if let FunctionBody::Statements(scope) = &node.body { - v.visit_scope(scope); - }; } pub fn visit_args<'ast, V>(v: &mut V, node: &'ast ArgList) diff --git a/tools/hangar/__snapshots__/invalid.ts.snap b/tools/hangar/__snapshots__/invalid.ts.snap index 005fcfcc15..b5887f03c0 100644 --- a/tools/hangar/__snapshots__/invalid.ts.snap +++ b/tools/hangar/__snapshots__/invalid.ts.snap @@ -341,7 +341,28 @@ error: Expected type to be \\"inflight (str): void\\", but got \\"inflight (num) | ^ Expected type to be \\"inflight (str): void\\", but got \\"inflight (num): void\\" instead -error: Class C (at ../../../examples/tests/invalid/impl_interface.w:16:10)'s implements \\"Bucket\\" is not an interface +error: Expected an interface, instead found type \\"Bucket\\" + + +error: Resource \\"r\\" does not implement method \\"method_1\\" of interface \\"I3\\" + --> ../../../examples/tests/invalid/impl_interface.w:33:10 + | +33 | resource r impl I3 { + | ^ Resource \\"r\\" does not implement method \\"method_1\\" of interface \\"I3\\" + + +error: Resource \\"r\\" does not implement method \\"method_2\\" of interface \\"I3\\" + --> ../../../examples/tests/invalid/impl_interface.w:33:10 + | +33 | resource r impl I3 { + | ^ Resource \\"r\\" does not implement method \\"method_2\\" of interface \\"I3\\" + + +error: Resource \\"r\\" does not implement method \\"method_3\\" of interface \\"I3\\" + --> ../../../examples/tests/invalid/impl_interface.w:33:10 + | +33 | resource r impl I3 { + | ^ Resource \\"r\\" does not implement method \\"method_3\\" of interface \\"I3\\" " `; @@ -400,6 +421,40 @@ exports[`inflight_ref_unknown_op.w > stderr 1`] = ` " `; +exports[`interface.w > stderr 1`] = ` +"error: Unknown symbol \\"IB\\" + --> ../../../examples/tests/invalid/interface.w:2:22 + | +2 | interface IA extends IB { + | ^^ Unknown symbol \\"IB\\" + + +error: Unknown symbol \\"IDontExist\\" + --> ../../../examples/tests/invalid/interface.w:10:26 + | +10 | interface IExist extends IDontExist { + | ^^^^^^^^^^ Unknown symbol \\"IDontExist\\" + + +error: Expected an interface, instead found type \\"ISomeClass\\" + + +error: Symbol \\"foo\\" already defined in this scope + --> ../../../examples/tests/invalid/interface.w:25:5 + | +25 | foo(); + | ^^^ Symbol \\"foo\\" already defined in this scope + + +error: Symbol \\"foo\\" already defined in this scope + --> ../../../examples/tests/invalid/interface.w:27:5 + | +27 | foo(): num; + | ^^^ Symbol \\"foo\\" already defined in this scope + +" +`; + exports[`json.w > stderr 1`] = ` "error: Expected type to be \\"str\\", but got \\"Json\\" instead --> ../../../examples/tests/invalid/json.w:5:14 diff --git a/tools/hangar/__snapshots__/test_corpus/valid/impl_interface.w.ts.snap b/tools/hangar/__snapshots__/test_corpus/valid/impl_interface.w.ts.snap index afd7aa4fe8..e4967e1f4d 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/impl_interface.w.ts.snap +++ b/tools/hangar/__snapshots__/test_corpus/valid/impl_interface.w.ts.snap @@ -14,6 +14,34 @@ async handle(msg) { exports.A = A;" `; +exports[`wing compile -t tf-aws > clients/Dog.inflight.js 1`] = ` +"class Dog { +constructor({ }) { + + +} +async eat() { + { + return; +} +}} +exports.Dog = Dog;" +`; + +exports[`wing compile -t tf-aws > clients/r.inflight.js 1`] = ` +"class r { +constructor({ }) { + + +} +async method_2(x) { + { + return x; +} +}} +exports.r = r;" +`; + exports[`wing compile -t tf-aws > main.tf.json 1`] = ` { "//": { @@ -74,6 +102,43 @@ constructor() { } } A._annotateInflight(\\"handle\\", {}); + class r extends $stdlib.core.Resource { + constructor(scope, id, ) { + super(scope, id); + { + } + } + method_1(x) { + { + return x; + } + } + method_3(x) { + { + return x; + } + } + _toInflight() { + + const self_client_path = \\"./clients/r.inflight.js\\".replace(/\\\\\\\\/g, \\"/\\"); + return $stdlib.core.NodeJsCode.fromInline(\`(new (require(\\"\${self_client_path}\\")).r({}))\`); + } + } + r._annotateInflight(\\"method_2\\", {}); + class Dog extends $stdlib.core.Resource { + constructor(scope, id, ) { + super(scope, id); + { + } + } + + _toInflight() { + + const self_client_path = \\"./clients/Dog.inflight.js\\".replace(/\\\\\\\\/g, \\"/\\"); + return $stdlib.core.NodeJsCode.fromInline(\`(new (require(\\"\${self_client_path}\\")).Dog({}))\`); + } + } + Dog._annotateInflight(\\"eat\\", {}); const x = new A(this,\\"A\\"); const y = new $stdlib.core.Inflight(this, \\"$Inflight1\\", { code: $stdlib.core.NodeJsCode.fromFile(require.resolve(\\"./proc.a5455e5c5848cfc293fcb861d9393d32e29a39f8dc8ac79c19dd5289279762fe/index.js\\".replace(/\\\\\\\\/g, \\"/\\"))), @@ -84,6 +149,7 @@ constructor() { }, } }); + const z = new Dog(this,\\"Dog\\"); } } new MyApp().synth();" diff --git a/tools/hangar/__snapshots__/test_corpus/valid/interface.w.ts.snap b/tools/hangar/__snapshots__/test_corpus/valid/interface.w.ts.snap new file mode 100644 index 0000000000..926632ed76 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/valid/interface.w.ts.snap @@ -0,0 +1,53 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`wing compile -t tf-aws > main.tf.json 1`] = ` +{ + "//": { + "metadata": { + "backend": "local", + "stackName": "root", + "version": "0.15.2", + }, + "outputs": {}, + }, + "provider": { + "aws": [ + {}, + ], + }, +} +`; + +exports[`wing compile -t tf-aws > preflight.js 1`] = ` +"const $stdlib = require('@winglang/sdk'); +const $outdir = process.env.WING_SYNTH_DIR ?? \\".\\"; + +function __app(target) { + switch (target) { + case \\"sim\\": + return $stdlib.sim.App; + case \\"tfaws\\": + case \\"tf-aws\\": + return $stdlib.tfaws.App; + case \\"tf-gcp\\": + return $stdlib.tfgcp.App; + case \\"tf-azure\\": + return $stdlib.tfazure.App; + case \\"awscdk\\": + return $stdlib.awscdk.App; + default: + throw new Error(\`Unknown WING_TARGET value: \\"\${process.env.WING_TARGET ?? \\"\\"}\\"\`); + } +} +const $App = __app(process.env.WING_TARGET); + +class MyApp extends $App { +constructor() { + super({ outdir: $outdir, name: \\"interface\\", plugins: $plugins }); + +} +} +new MyApp().synth();" +`; + +exports[`wing test > stdout 1`] = `"pass ─ interface.w (no tests)"`;