Skip to content

Commit

Permalink
Merge of #4210
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Sep 27, 2023
2 parents c0b8744 + f82b5d9 commit 2eeef63
Show file tree
Hide file tree
Showing 30 changed files with 1,142 additions and 257 deletions.
23 changes: 23 additions & 0 deletions docs/docs/03-language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,29 @@ bring cloud; // from cloud bring * as cloud;
bring "cdktf" as cdktf; // from "cdktf" bring * as cdktf;
```
To import an individual Wing file as a module, you can specify its path relative
to the current file:
```TS
bring "./my-module.w" as myModule;
```
It's also possible to import a directory as a module. The module will contain all
public types defined in the directory's files. If the directory has subdirectories,
they will be available under the corresponding names.
```TS
bring "./my-module" as myModule;
// from ./my-module/submodule/my-class.w
new myModule.submodule.MyClass();
```
The following features are not yet implemented, but we are planning to add them in the future:
* Specify types as public using `pub` - see https://github.com/winglang/wing/issues/4294 to track.
* Specify types as public within the current project or library, and private outside, using `internal` - see https://github.com/winglang/wing/issues/4156 to track.
[`top`][top]
---
Expand Down
5 changes: 5 additions & 0 deletions examples/tests/invalid/bring_local_dir.test.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bring "./subdir" as subdir;
// Error: "Foo" is defined multiple times in "./subdir"

bring "/subdir" as baz;
// ^ error: Cannot bring "/subdir" as it is not a relative path
6 changes: 6 additions & 0 deletions examples/tests/invalid/bring_local_self.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ bring "./bring_local_self.main.w" as foo;

bring "./non-existent.w" as bar;
// ^ error: Could not find Wing module "./non-existent.w"

bring "/hello.w" as baz;
// ^ error: Cannot bring "/hello.w" as it is not a relative path

bring "./bring_local_dir.test.w" as qux;
// ^ error: Cannot bring "./main.w": entrypoint files cannot be imported
1 change: 1 addition & 0 deletions examples/tests/invalid/subdir/inner/foo1.w
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class Foo {}
1 change: 1 addition & 0 deletions examples/tests/invalid/subdir/inner/foo2.w
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class Foo {}
2 changes: 2 additions & 0 deletions examples/tests/invalid/subdir/other.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bring "./inner" as inner;
// Error: "Foo" is defined multiple times in "./inner"
9 changes: 0 additions & 9 deletions examples/tests/valid/bring_jsii_path.test.w

This file was deleted.

19 changes: 19 additions & 0 deletions examples/tests/valid/bring_local_dir.test.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
bring "./subdir2/inner/widget.w" as w;
bring "./subdir2" as subdir;

let widget1 = new w.Widget();
assert(widget1.compute() == 42);

// from subdir/file1.w
let foo = new subdir.Foo();
assert(foo.foo() == "foo");

// from subdir/file2.w
let bar = new subdir.Bar();
assert(bar.bar() == "bar");

// from subdir/inner/widget.w
let widget2 = new subdir.inner.Widget();
assert(widget2.compute() == 42);

assert(foo.checkWidget(widget2) == 1379);
11 changes: 11 additions & 0 deletions examples/tests/valid/subdir2/file1.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
bring "./inner" as blah; // an alias that is not "inner"

class Foo {
pub foo(): str {
return "foo";
}

pub checkWidget(widget: blah.Widget): num {
return widget.compute() + blah.Widget.staticCompute();
}
}
5 changes: 5 additions & 0 deletions examples/tests/valid/subdir2/file2.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Bar {
pub bar(): str {
return "bar";
}
}
9 changes: 9 additions & 0 deletions examples/tests/valid/subdir2/inner/widget.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Widget {
pub compute(): num {
return 42;
}

pub static staticCompute(): num {
return 1337;
}
}
7 changes: 4 additions & 3 deletions libs/wingc/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,22 +316,20 @@ pub struct Stmt {
#[derive(Debug)]
pub enum UtilityFunctions {
Log,
Throw,
Assert,
}

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

impl Display for UtilityFunctions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UtilityFunctions::Log => write!(f, "log"),
UtilityFunctions::Throw => write!(f, "throw"),
UtilityFunctions::Assert => write!(f, "assert"),
}
}
Expand Down Expand Up @@ -428,7 +426,10 @@ pub struct Interface {
pub enum BringSource {
BuiltinModule(Symbol),
JsiiModule(Symbol),
/// Refers to a relative path to a file
WingFile(Symbol),
/// Refers to a relative path to a directory
Directory(Symbol),
}

#[derive(Debug)]
Expand Down
14 changes: 13 additions & 1 deletion libs/wingc/src/file_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ impl FileGraph {
self.path_to_node_index.contains_key(path)
}

/// Returns a list of the direct dependencies of the given file.
/// (does not include all transitive dependencies)
/// The file path must be relative to the root of the file graph.
pub fn dependencies_of(&self, path: &Utf8Path) -> Vec<&Utf8PathBuf> {
let node_index = self.path_to_node_index.get(path).expect("path not in graph");
self
.graph
.edges(*node_index)
.map(|edge| &self.graph[edge.target()])
.collect::<Vec<_>>()
}

/// Returns a list of files in the order they should be compiled
/// Or a list of files that are part of a cycle, if one exists
pub fn toposort(&self) -> Result<Vec<Utf8PathBuf>, Vec<Utf8PathBuf>> {
Expand Down Expand Up @@ -86,7 +98,7 @@ impl Display for FileGraph {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for node_index in self.graph.node_indices() {
let node = &self.graph[node_index];
write!(f, "{{{} -> [", node)?;
write!(f, "{{{} -> [ ", node)?;
for edge in self.graph.edges(node_index) {
let target = &self.graph[edge.target()];
write!(f, "{} ", target)?;
Expand Down
1 change: 1 addition & 0 deletions libs/wingc/src/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ where
BringSource::BuiltinModule(name) => BringSource::BuiltinModule(f.fold_symbol(name)),
BringSource::JsiiModule(name) => BringSource::JsiiModule(f.fold_symbol(name)),
BringSource::WingFile(name) => BringSource::WingFile(f.fold_symbol(name)),
BringSource::Directory(name) => BringSource::Directory(f.fold_symbol(name)),
},
identifier: identifier.map(|id| f.fold_symbol(id)),
},
Expand Down
45 changes: 45 additions & 0 deletions libs/wingc/src/jsify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::{
comp_ctx::{CompilationContext, CompilationPhase},
dbg_panic, debug,
diagnostic::{report_diagnostic, Diagnostic, WingSpan},
file_graph::FileGraph,
files::Files,
type_check::{
is_udt_struct_type,
Expand Down Expand Up @@ -68,6 +69,7 @@ pub struct JSifier<'a> {
/// e.g. "bucket.w" -> "preflight.bucket-1.js"
preflight_file_map: RefCell<IndexMap<Utf8PathBuf, String>>,
source_files: &'a Files,
source_file_graph: &'a FileGraph,
/// Root of the project, used for resolving extern modules
absolute_project_root: &'a Utf8Path,
/// The entrypoint file of the Wing application.
Expand All @@ -86,13 +88,15 @@ impl<'a> JSifier<'a> {
pub fn new(
types: &'a mut Types,
source_files: &'a Files,
source_file_graph: &'a FileGraph,
entrypoint_file_path: &'a Utf8Path,
absolute_project_root: &'a Utf8Path,
) -> Self {
let output_files = Files::default();
Self {
types,
source_files,
source_file_graph,
entrypoint_file_path,
absolute_project_root,
referenced_struct_schemas: RefCell::new(BTreeMap::new()),
Expand Down Expand Up @@ -138,6 +142,7 @@ impl<'a> JSifier<'a> {
let mut output = CodeMaker::default();

let is_entrypoint_file = source_path == self.entrypoint_file_path;
let is_directory = source_path.is_dir();

if is_entrypoint_file {
output.line(format!("const {} = require('{}');", STDLIB, STDLIB_MODULE));
Expand Down Expand Up @@ -175,6 +180,35 @@ impl<'a> JSifier<'a> {
"new $App({{ outdir: {}, name: \"{}\", rootConstruct: {}, plugins: {}, isTestEnvironment: {}, entrypointDir: process.env['WING_SOURCE_DIR'], rootId: process.env['WING_ROOT_ID'] }}).synth();",
OUTDIR_VAR, app_name, ROOT_CLASS, PLUGINS_VAR, ENV_WING_IS_TEST
));
} else if is_directory {
let directory_children = self.source_file_graph.dependencies_of(source_path);
let preflight_file_map = self.preflight_file_map.borrow();

// supposing a directory has two files and two subdirectories in it,
// we generate code like this:
// ```
// return {
// inner_directory1: require("./preflight.inner-directory1.js")({ $stdlib }),
// inner_directory2: require("./preflight.inner-directory2.js")({ $stdlib }),
// ...require("./preflight.inner-file1.js")({ $stdlib }),
// ...require("./preflight.inner-file2.js")({ $stdlib }),
// };
// ```
output.open("return {");
for file in directory_children {
let preflight_file_name = preflight_file_map.get(file).expect("no emitted JS file found");
if file.is_dir() {
let directory_name = file.file_stem().unwrap();
output.line(format!(
"{}: require(\"./{}\")({{ {} }}),",
directory_name, preflight_file_name, STDLIB
));
} else {
output.line(format!("...require(\"./{}\")({{ {} }}),", preflight_file_name, STDLIB));
}
}
output.close("};");
output.close("};");
} else {
output.add_code(js);
let exports = get_public_symbols(&scope);
Expand Down Expand Up @@ -745,6 +779,17 @@ impl<'a> JSifier<'a> {
STDLIB,
))
}
BringSource::Directory(name) => {
let preflight_file_map = self.preflight_file_map.borrow();
let preflight_file_name = preflight_file_map.get(Utf8Path::new(&name.name)).unwrap();
CodeMaker::one_line(format!(
"const {} = require(\"./{}\")({{ {} }});",
// checked during type checking
identifier.as_ref().expect("bring wing file requires an alias"),
preflight_file_name,
STDLIB,
))
}
},
StmtKind::SuperConstructor { arg_list } => {
let args = self.jsify_arg_list(&arg_list, None, None, ctx);
Expand Down
34 changes: 23 additions & 11 deletions libs/wingc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ const WINGSDK_AWS_MODULE: &'static str = "aws";
const WINGSDK_EX_MODULE: &'static str = "ex";
const WINGSDK_REGEX_MODULE: &'static str = "regex";

pub const UTIL_CLASS_NAME: &'static str = "Util";

const WINGSDK_BRINGABLE_MODULES: [&'static str; 7] = [
WINGSDK_CLOUD_MODULE,
WINGSDK_UTIL_MODULE,
Expand Down Expand Up @@ -105,6 +107,8 @@ 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 struct CompilerOutput {}

/// Exposes an allocation function to the WASM host
Expand Down Expand Up @@ -197,6 +201,7 @@ pub fn type_check(
scope: &mut Scope,
types: &mut Types,
file_path: &Utf8Path,
file_graph: &FileGraph,
jsii_types: &mut TypeSystem,
jsii_imports: &mut Vec<JsiiImportSpec>,
) {
Expand Down Expand Up @@ -245,7 +250,7 @@ pub fn type_check(
);

let mut scope_env = types.get_scope_env(&scope);
let mut tc = TypeChecker::new(types, file_path, jsii_types, jsii_imports);
let mut tc = TypeChecker::new(types, file_path, file_graph, jsii_types, jsii_imports);
tc.add_module_to_env(
&mut scope_env,
WINGSDK_ASSEMBLY_NAME.to_string(),
Expand All @@ -254,7 +259,7 @@ pub fn type_check(
None,
);

tc.type_check_file(file_path, scope);
tc.type_check_file_or_dir(file_path, scope);
}

// TODO: refactor this (why is scope needed?) (move to separate module?)
Expand Down Expand Up @@ -315,11 +320,18 @@ pub fn compile(
// Create a universal JSII import spec (need to keep this alive during entire compilation)
let mut jsii_imports = vec![];

// Type check all files in topological order (start with files that don't require any other
// Wing files, then move on to files that depend on those, etc.)
// Type check all files in topological order (start with files that don't bring any other
// Wing files, then move on to files that depend on those, and repeat)
for file in &topo_sorted_files {
let mut scope = asts.get_mut(file).expect("matching AST not found");
type_check(&mut scope, &mut types, &file, &mut jsii_types, &mut jsii_imports);
type_check(
&mut scope,
&mut types,
&file,
&file_graph,
&mut jsii_types,
&mut jsii_imports,
);

// Validate the type checker didn't miss anything - see `TypeCheckAssert` for details
let mut tc_assert = TypeCheckAssert::new(&types, found_errors());
Expand All @@ -335,15 +347,15 @@ pub fn compile(
.to_path_buf();

// Verify that the project dir is absolute
if !is_project_dir_absolute(&project_dir) {
if !is_absolute_path(&project_dir) {
report_diagnostic(Diagnostic {
message: format!("Project directory must be absolute: {}", project_dir),
span: None,
});
return Err(());
}

let mut jsifier = JSifier::new(&mut types, &files, &source_path, &project_dir);
let mut jsifier = JSifier::new(&mut types, &files, &file_graph, &source_path, &project_dir);

// -- LIFTING PHASE --

Expand Down Expand Up @@ -392,15 +404,15 @@ pub fn compile(
return Ok(CompilerOutput {});
}

fn is_project_dir_absolute(project_dir: &Utf8PathBuf) -> bool {
if project_dir.starts_with("/") {
pub fn is_absolute_path(path: &Utf8Path) -> bool {
if path.starts_with("/") {
return true;
}

let project_dir = project_dir.as_str();
// Check if this is a Windows path instead by checking if the second char is a colon
// Note: Cannot use Utf8Path::is_absolute() because it doesn't work with Windows paths on WASI
if project_dir.len() < 2 || project_dir.chars().nth(1).expect("Project dir has second character") != ':' {
let chars = path.as_str().chars().collect::<Vec<char>>();
if chars.len() < 2 || chars[1] != ':' {
return false;
}

Expand Down
Loading

0 comments on commit 2eeef63

Please sign in to comment.