mirror of
https://github.com/roc-lang/roc.git
synced 2025-08-03 03:42:17 +00:00
Merge pull request #3462 from rtfeldman/fix-nested-imports
Fix importing interfaces with nested paths
This commit is contained in:
commit
3b60acb938
24 changed files with 109 additions and 94 deletions
|
@ -40,7 +40,7 @@ fn write_subs_for_module(module_id: ModuleId, filename: &str) {
|
|||
&arena,
|
||||
PathBuf::from(filename),
|
||||
source,
|
||||
&src_dir,
|
||||
src_dir,
|
||||
Default::default(),
|
||||
target_info,
|
||||
roc_reporting::report::RenderTarget::ColorTerminal,
|
||||
|
|
|
@ -7,7 +7,7 @@ use roc_module::symbol::{ModuleId, Symbol};
|
|||
use roc_reporting::report::RenderTarget;
|
||||
use roc_target::TargetInfo;
|
||||
use roc_types::subs::{Subs, Variable};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use roc_load_internal::docs;
|
||||
pub use roc_load_internal::file::{
|
||||
|
@ -18,7 +18,6 @@ pub use roc_load_internal::file::{
|
|||
fn load<'a>(
|
||||
arena: &'a Bump,
|
||||
load_start: LoadStart<'a>,
|
||||
src_dir: &Path,
|
||||
exposed_types: ExposedByModule,
|
||||
goal_phase: Phase,
|
||||
target_info: TargetInfo,
|
||||
|
@ -30,7 +29,6 @@ fn load<'a>(
|
|||
roc_load_internal::file::load(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
goal_phase,
|
||||
target_info,
|
||||
|
@ -44,7 +42,6 @@ fn load<'a>(
|
|||
pub fn load_single_threaded<'a>(
|
||||
arena: &'a Bump,
|
||||
load_start: LoadStart<'a>,
|
||||
src_dir: &Path,
|
||||
exposed_types: ExposedByModule,
|
||||
goal_phase: Phase,
|
||||
target_info: TargetInfo,
|
||||
|
@ -55,7 +52,6 @@ pub fn load_single_threaded<'a>(
|
|||
roc_load_internal::file::load_single_threaded(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
goal_phase,
|
||||
target_info,
|
||||
|
@ -69,7 +65,7 @@ pub fn load_and_monomorphize_from_str<'a>(
|
|||
arena: &'a Bump,
|
||||
filename: PathBuf,
|
||||
src: &'a str,
|
||||
src_dir: &Path,
|
||||
src_dir: PathBuf,
|
||||
exposed_types: ExposedByModule,
|
||||
target_info: TargetInfo,
|
||||
render: RenderTarget,
|
||||
|
@ -77,12 +73,11 @@ pub fn load_and_monomorphize_from_str<'a>(
|
|||
) -> Result<MonomorphizedModule<'a>, LoadingProblem<'a>> {
|
||||
use LoadResult::*;
|
||||
|
||||
let load_start = LoadStart::from_str(arena, filename, src)?;
|
||||
let load_start = LoadStart::from_str(arena, filename, src, src_dir)?;
|
||||
|
||||
match load(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
Phase::MakeSpecializations,
|
||||
target_info,
|
||||
|
@ -94,23 +89,22 @@ pub fn load_and_monomorphize_from_str<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn load_and_monomorphize<'a>(
|
||||
arena: &'a Bump,
|
||||
pub fn load_and_monomorphize(
|
||||
arena: &Bump,
|
||||
filename: PathBuf,
|
||||
src_dir: &Path,
|
||||
src_dir: PathBuf,
|
||||
exposed_types: ExposedByModule,
|
||||
target_info: TargetInfo,
|
||||
render: RenderTarget,
|
||||
threading: Threading,
|
||||
) -> Result<MonomorphizedModule<'a>, LoadingProblem<'a>> {
|
||||
) -> Result<MonomorphizedModule<'_>, LoadingProblem<'_>> {
|
||||
use LoadResult::*;
|
||||
|
||||
let load_start = LoadStart::from_path(arena, filename, render)?;
|
||||
let load_start = LoadStart::from_path(arena, src_dir, filename, render)?;
|
||||
|
||||
match load(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
Phase::MakeSpecializations,
|
||||
target_info,
|
||||
|
@ -122,23 +116,22 @@ pub fn load_and_monomorphize<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn load_and_typecheck<'a>(
|
||||
arena: &'a Bump,
|
||||
pub fn load_and_typecheck(
|
||||
arena: &Bump,
|
||||
filename: PathBuf,
|
||||
src_dir: &Path,
|
||||
src_dir: PathBuf,
|
||||
exposed_types: ExposedByModule,
|
||||
target_info: TargetInfo,
|
||||
render: RenderTarget,
|
||||
threading: Threading,
|
||||
) -> Result<LoadedModule, LoadingProblem<'a>> {
|
||||
) -> Result<LoadedModule, LoadingProblem<'_>> {
|
||||
use LoadResult::*;
|
||||
|
||||
let load_start = LoadStart::from_path(arena, filename, render)?;
|
||||
let load_start = LoadStart::from_path(arena, src_dir, filename, render)?;
|
||||
|
||||
match load(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
Phase::SolveTypes,
|
||||
target_info,
|
||||
|
@ -154,14 +147,14 @@ pub fn load_and_typecheck_str<'a>(
|
|||
arena: &'a Bump,
|
||||
filename: PathBuf,
|
||||
source: &'a str,
|
||||
src_dir: &Path,
|
||||
src_dir: PathBuf,
|
||||
exposed_types: ExposedByModule,
|
||||
target_info: TargetInfo,
|
||||
render: RenderTarget,
|
||||
) -> Result<LoadedModule, LoadingProblem<'a>> {
|
||||
use LoadResult::*;
|
||||
|
||||
let load_start = LoadStart::from_str(arena, filename, source)?;
|
||||
let load_start = LoadStart::from_str(arena, filename, source, src_dir)?;
|
||||
|
||||
// NOTE: this function is meant for tests, and so we use single-threaded
|
||||
// solving so we don't use too many threads per-test. That gives higher
|
||||
|
@ -169,7 +162,6 @@ pub fn load_and_typecheck_str<'a>(
|
|||
match load_single_threaded(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
Phase::SolveTypes,
|
||||
target_info,
|
||||
|
|
|
@ -1067,7 +1067,7 @@ pub fn load_and_typecheck_str<'a>(
|
|||
arena: &'a Bump,
|
||||
filename: PathBuf,
|
||||
source: &'a str,
|
||||
src_dir: &Path,
|
||||
src_dir: PathBuf,
|
||||
exposed_types: ExposedByModule,
|
||||
target_info: TargetInfo,
|
||||
render: RenderTarget,
|
||||
|
@ -1075,7 +1075,7 @@ pub fn load_and_typecheck_str<'a>(
|
|||
) -> Result<LoadedModule, LoadingProblem<'a>> {
|
||||
use LoadResult::*;
|
||||
|
||||
let load_start = LoadStart::from_str(arena, filename, source)?;
|
||||
let load_start = LoadStart::from_str(arena, filename, source, src_dir)?;
|
||||
|
||||
// this function is used specifically in the case
|
||||
// where we want to regenerate the cached data
|
||||
|
@ -1084,7 +1084,6 @@ pub fn load_and_typecheck_str<'a>(
|
|||
match load(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
Phase::SolveTypes,
|
||||
target_info,
|
||||
|
@ -1108,11 +1107,13 @@ pub struct LoadStart<'a> {
|
|||
ident_ids_by_module: SharedIdentIdsByModule,
|
||||
root_id: ModuleId,
|
||||
root_msg: Msg<'a>,
|
||||
src_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl<'a> LoadStart<'a> {
|
||||
pub fn from_path(
|
||||
arena: &'a Bump,
|
||||
mut src_dir: PathBuf,
|
||||
filename: PathBuf,
|
||||
render: RenderTarget,
|
||||
) -> Result<Self, LoadingProblem<'a>> {
|
||||
|
@ -1135,7 +1136,32 @@ impl<'a> LoadStart<'a> {
|
|||
);
|
||||
|
||||
match res_loaded {
|
||||
Ok(good) => good,
|
||||
Ok((module_id, msg)) => {
|
||||
if let Msg::Header(ModuleHeader {
|
||||
module_id: header_id,
|
||||
module_name,
|
||||
is_root_module,
|
||||
..
|
||||
}) = &msg
|
||||
{
|
||||
debug_assert_eq!(*header_id, module_id);
|
||||
debug_assert!(is_root_module);
|
||||
|
||||
if let ModuleNameEnum::Interface(name) = module_name {
|
||||
// Interface modules can have names like Foo.Bar.Baz,
|
||||
// in which case we need to adjust the src_dir to
|
||||
// remove the "Bar/Baz" directories in order to correctly
|
||||
// resolve this interface module's imports!
|
||||
let dirs_to_pop = name.as_str().matches('.').count();
|
||||
|
||||
for _ in 0..dirs_to_pop {
|
||||
src_dir.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(module_id, msg)
|
||||
}
|
||||
|
||||
Err(LoadingProblem::ParsingFailed(problem)) => {
|
||||
let module_ids = Arc::try_unwrap(arc_modules)
|
||||
|
@ -1166,6 +1192,7 @@ impl<'a> LoadStart<'a> {
|
|||
Ok(LoadStart {
|
||||
arc_modules,
|
||||
ident_ids_by_module,
|
||||
src_dir,
|
||||
root_id,
|
||||
root_msg,
|
||||
})
|
||||
|
@ -1175,6 +1202,7 @@ impl<'a> LoadStart<'a> {
|
|||
arena: &'a Bump,
|
||||
filename: PathBuf,
|
||||
src: &'a str,
|
||||
src_dir: PathBuf,
|
||||
) -> Result<Self, LoadingProblem<'a>> {
|
||||
let arc_modules = Arc::new(Mutex::new(PackageModuleIds::default()));
|
||||
let root_exposed_ident_ids = IdentIds::exposed_builtins(0);
|
||||
|
@ -1196,6 +1224,7 @@ impl<'a> LoadStart<'a> {
|
|||
|
||||
Ok(LoadStart {
|
||||
arc_modules,
|
||||
src_dir,
|
||||
ident_ids_by_module,
|
||||
root_id,
|
||||
root_msg,
|
||||
|
@ -1269,7 +1298,6 @@ pub enum Threading {
|
|||
pub fn load<'a>(
|
||||
arena: &'a Bump,
|
||||
load_start: LoadStart<'a>,
|
||||
src_dir: &Path,
|
||||
exposed_types: ExposedByModule,
|
||||
goal_phase: Phase,
|
||||
target_info: TargetInfo,
|
||||
|
@ -1305,7 +1333,6 @@ pub fn load<'a>(
|
|||
Threads::Single => load_single_threaded(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
goal_phase,
|
||||
target_info,
|
||||
|
@ -1315,7 +1342,6 @@ pub fn load<'a>(
|
|||
Threads::Many(threads) => load_multi_threaded(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
goal_phase,
|
||||
target_info,
|
||||
|
@ -1331,7 +1357,6 @@ pub fn load<'a>(
|
|||
pub fn load_single_threaded<'a>(
|
||||
arena: &'a Bump,
|
||||
load_start: LoadStart<'a>,
|
||||
src_dir: &Path,
|
||||
exposed_types: ExposedByModule,
|
||||
goal_phase: Phase,
|
||||
target_info: TargetInfo,
|
||||
|
@ -1343,6 +1368,7 @@ pub fn load_single_threaded<'a>(
|
|||
ident_ids_by_module,
|
||||
root_id,
|
||||
root_msg,
|
||||
src_dir,
|
||||
..
|
||||
} = load_start;
|
||||
|
||||
|
@ -1394,7 +1420,7 @@ pub fn load_single_threaded<'a>(
|
|||
stealers,
|
||||
&worker_msg_rx,
|
||||
&msg_tx,
|
||||
src_dir,
|
||||
&src_dir,
|
||||
target_info,
|
||||
);
|
||||
|
||||
|
@ -1532,7 +1558,6 @@ fn state_thread_step<'a>(
|
|||
fn load_multi_threaded<'a>(
|
||||
arena: &'a Bump,
|
||||
load_start: LoadStart<'a>,
|
||||
src_dir: &Path,
|
||||
exposed_types: ExposedByModule,
|
||||
goal_phase: Phase,
|
||||
target_info: TargetInfo,
|
||||
|
@ -1545,6 +1570,7 @@ fn load_multi_threaded<'a>(
|
|||
ident_ids_by_module,
|
||||
root_id,
|
||||
root_msg,
|
||||
src_dir,
|
||||
..
|
||||
} = load_start;
|
||||
|
||||
|
@ -1622,8 +1648,9 @@ fn load_multi_threaded<'a>(
|
|||
|
||||
// We only want to move a *reference* to the main task queue's
|
||||
// injector in the thread, not the injector itself
|
||||
// (since other threads need to reference it too).
|
||||
// (since other threads need to reference it too). Same with src_dir.
|
||||
let injector = &injector;
|
||||
let src_dir = &src_dir;
|
||||
|
||||
// Record this thread's handle so the main thread can join it later.
|
||||
let res_join_handle = thread_scope
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
interface Dep3.Blah
|
||||
exposes [one, two, foo, bar]
|
||||
imports []
|
||||
imports [Dep3.Other]
|
||||
|
||||
one = 1
|
||||
|
||||
two = 2
|
||||
|
||||
foo = "foo from Dep3"
|
||||
bar = "bar from Dep3"
|
||||
bar = Dep3.Other.bar
|
||||
|
|
6
crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Other.roc
vendored
Normal file
6
crates/compiler/load_internal/tests/fixtures/build/app_with_deps/Dep3/Other.roc
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
interface Dep3.Other
|
||||
exposes [foo, bar]
|
||||
imports []
|
||||
|
||||
foo = "foo from Dep3.Other"
|
||||
bar = "bar from Dep3.Other"
|
|
@ -30,23 +30,22 @@ use roc_target::TargetInfo;
|
|||
use roc_types::pretty_print::name_and_print_var;
|
||||
use roc_types::pretty_print::DebugPrint;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn load_and_typecheck<'a>(
|
||||
arena: &'a Bump,
|
||||
fn load_and_typecheck(
|
||||
arena: &Bump,
|
||||
filename: PathBuf,
|
||||
src_dir: &Path,
|
||||
src_dir: PathBuf,
|
||||
exposed_types: ExposedByModule,
|
||||
target_info: TargetInfo,
|
||||
) -> Result<LoadedModule, LoadingProblem<'a>> {
|
||||
) -> Result<LoadedModule, LoadingProblem> {
|
||||
use LoadResult::*;
|
||||
|
||||
let load_start = LoadStart::from_path(arena, filename, RenderTarget::Generic)?;
|
||||
let load_start = LoadStart::from_path(arena, src_dir, filename, RenderTarget::Generic)?;
|
||||
|
||||
match roc_load_internal::file::load(
|
||||
arena,
|
||||
load_start,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
Phase::SolveTypes,
|
||||
target_info,
|
||||
|
@ -167,7 +166,7 @@ fn multiple_modules_help<'a>(
|
|||
load_and_typecheck(
|
||||
arena,
|
||||
full_file_path,
|
||||
dir.path(),
|
||||
dir.path().to_path_buf(),
|
||||
Default::default(),
|
||||
TARGET_INFO,
|
||||
)
|
||||
|
@ -184,13 +183,7 @@ fn load_fixture(
|
|||
let src_dir = fixtures_dir().join(dir_name);
|
||||
let filename = src_dir.join(format!("{}.roc", module_name));
|
||||
let arena = Bump::new();
|
||||
let loaded = load_and_typecheck(
|
||||
&arena,
|
||||
filename,
|
||||
src_dir.as_path(),
|
||||
subs_by_module,
|
||||
TARGET_INFO,
|
||||
);
|
||||
let loaded = load_and_typecheck(&arena, filename, src_dir, subs_by_module, TARGET_INFO);
|
||||
let mut loaded_module = match loaded {
|
||||
Ok(x) => x,
|
||||
Err(roc_load_internal::file::LoadingProblem::FormattedReport(report)) => {
|
||||
|
@ -346,13 +339,7 @@ fn interface_with_deps() {
|
|||
let src_dir = fixtures_dir().join("interface_with_deps");
|
||||
let filename = src_dir.join("Primary.roc");
|
||||
let arena = Bump::new();
|
||||
let loaded = load_and_typecheck(
|
||||
&arena,
|
||||
filename,
|
||||
src_dir.as_path(),
|
||||
subs_by_module,
|
||||
TARGET_INFO,
|
||||
);
|
||||
let loaded = load_and_typecheck(&arena, filename, src_dir, subs_by_module, TARGET_INFO);
|
||||
|
||||
let mut loaded_module = loaded.expect("Test module failed to load");
|
||||
let home = loaded_module.module_id;
|
||||
|
|
|
@ -98,7 +98,7 @@ mod solve_expr {
|
|||
arena,
|
||||
file_path,
|
||||
module_src,
|
||||
dir.path(),
|
||||
dir.path().to_path_buf(),
|
||||
exposed_types,
|
||||
roc_target::TargetInfo::default_x86_64(),
|
||||
roc_reporting::report::RenderTarget::Generic,
|
||||
|
@ -6575,7 +6575,7 @@ mod solve_expr {
|
|||
A := {}
|
||||
id1 = \@A {} -> @A {}
|
||||
#^^^{-1}
|
||||
|
||||
|
||||
id2 = \@A {} -> id1 (@A {})
|
||||
#^^^{-1} ^^^
|
||||
|
||||
|
@ -6922,7 +6922,7 @@ mod solve_expr {
|
|||
Ok u -> [Pair u (List.drop inp 1)]
|
||||
_ -> []
|
||||
|
||||
main = any
|
||||
main = any
|
||||
"#
|
||||
),
|
||||
"Parser U8",
|
||||
|
|
|
@ -234,7 +234,7 @@ where
|
|||
&arena,
|
||||
encode_path().file_name().unwrap().into(),
|
||||
source,
|
||||
encode_path().parent().unwrap(),
|
||||
encode_path().parent().unwrap().to_path_buf(),
|
||||
Default::default(),
|
||||
target_info,
|
||||
roc_reporting::report::RenderTarget::ColorTerminal,
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
#[cfg(feature = "gen-llvm")]
|
||||
use crate::helpers::llvm::assert_evals_to;
|
||||
|
||||
#[cfg(feature = "gen-dev")]
|
||||
use crate::helpers::dev::assert_evals_to;
|
||||
|
||||
#[cfg(feature = "gen-wasm")]
|
||||
use crate::helpers::wasm::assert_evals_to;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))]
|
||||
use indoc::indoc;
|
||||
|
||||
#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))]
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::helpers::wasm::{assert_evals_to, expect_runtime_error_panic};
|
|||
// use crate::assert_wasm_evals_to as assert_evals_to;
|
||||
use indoc::indoc;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))]
|
||||
use roc_std::{RocList, RocStr};
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -9,7 +9,8 @@ use crate::helpers::wasm::assert_evals_to;
|
|||
|
||||
#[cfg(test)]
|
||||
use indoc::indoc;
|
||||
#[cfg(test)]
|
||||
|
||||
#[cfg(all(test, any(feature = "gen-llvm", feature = "gen-wasm")))]
|
||||
use roc_std::{RocList, RocStr};
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
use libloading::Library;
|
||||
use roc_build::link::{link, LinkType};
|
||||
use roc_builtins::bitcode;
|
||||
use roc_collections::all::MutMap;
|
||||
use roc_load::Threading;
|
||||
use roc_region::all::LineInfo;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
|
||||
use roc_collections::all::MutMap;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use roc_mono::ir::pretty_print_ir_symbols;
|
||||
|
||||
|
@ -30,11 +32,11 @@ pub fn helper(
|
|||
_leak: bool,
|
||||
lazy_literals: bool,
|
||||
) -> (String, Vec<roc_problem::can::Problem>, Library) {
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let filename = PathBuf::from("Test.roc");
|
||||
let src_dir = Path::new("fake/test/path");
|
||||
let src_dir = PathBuf::from("fake/test/path");
|
||||
let app_o_file = dir.path().join("app.o");
|
||||
|
||||
let module_src;
|
||||
|
|
|
@ -39,12 +39,12 @@ fn create_llvm_module<'a>(
|
|||
context: &'a inkwell::context::Context,
|
||||
target: &Triple,
|
||||
) -> (&'static str, String, &'a Module<'a>) {
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
let target_info = roc_target::TargetInfo::from(target);
|
||||
|
||||
let filename = PathBuf::from("Test.roc");
|
||||
let src_dir = Path::new("fake/test/path");
|
||||
let src_dir = PathBuf::from("fake/test/path");
|
||||
|
||||
let module_src;
|
||||
let temp;
|
||||
|
|
|
@ -73,7 +73,7 @@ fn compile_roc_to_wasm_bytes<'a, T: Wasm32Result>(
|
|||
_test_wrapper_type_info: PhantomData<T>,
|
||||
) -> Vec<u8> {
|
||||
let filename = PathBuf::from("Test.roc");
|
||||
let src_dir = Path::new("fake/test/path");
|
||||
let src_dir = PathBuf::from("fake/test/path");
|
||||
|
||||
let module_src;
|
||||
let temp;
|
||||
|
|
|
@ -73,12 +73,12 @@ fn promote_expr_to_module(src: &str) -> String {
|
|||
|
||||
fn compiles_to_ir(test_name: &str, src: &str) {
|
||||
use bumpalo::Bump;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
let arena = &Bump::new();
|
||||
|
||||
let filename = PathBuf::from("Test.roc");
|
||||
let src_dir = Path::new("fake/test/path");
|
||||
let src_dir = PathBuf::from("fake/test/path");
|
||||
|
||||
let module_src;
|
||||
let temp;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue