From 40fee86878068d5c7c833655b7637820fb2cba33 Mon Sep 17 00:00:00 2001 From: Jeff Bezanson Date: Thu, 8 Oct 2020 13:16:25 -0400 Subject: [PATCH] add `import ... as ...` syntax. closes #1255 (#37396) --- NEWS.md | 2 ++ base/show.jl | 9 +++++++- doc/src/manual/modules.md | 37 +++++++++++++++++++++++++++--- src/ast.c | 3 ++- src/ast.scm | 33 +++++++++++++++------------ src/julia-parser.scm | 27 +++++++++++++++++----- src/julia.h | 5 ++-- src/julia_internal.h | 2 +- src/module.c | 40 +++++++++++++++++++++++--------- src/toplevel.c | 47 +++++++++++++++++++++++++++++++------- test/show.jl | 5 ++++ test/syntax.jl | 48 +++++++++++++++++++++++++++++++++++++++ 12 files changed, 210 insertions(+), 48 deletions(-) diff --git a/NEWS.md b/NEWS.md index 774c837974c94..edd474671f359 100644 --- a/NEWS.md +++ b/NEWS.md @@ -21,6 +21,8 @@ New language features `(x...) -> (op).(x...)`, which can be useful for passing the broadcasted version of an operator to higher-order functions, like for example `map(.*, A, B)` for an elementwise product of two arrays of arrays. ([#37583]) +* The syntax `import A as B` (plus `import A: x as y`, `import A.x as y`, and `using A: x as y`) + can now be used to rename imported modules and identifiers ([#1255]). Language changes ---------------- diff --git a/base/show.jl b/base/show.jl index 2e6e82c74c403..fdc044ba984b0 100644 --- a/base/show.jl +++ b/base/show.jl @@ -1447,7 +1447,10 @@ function show_generator(io, ex::Expr, indent, quote_level) end end -function valid_import_path(@nospecialize ex) +function valid_import_path(@nospecialize(ex), allow_as = true) + if allow_as && is_expr(ex, :as) && length((ex::Expr).args) == 2 + ex = (ex::Expr).args[1] + end return is_expr(ex, :(.)) && length((ex::Expr).args) > 0 && all(a->isa(a,Symbol), (ex::Expr).args) end @@ -1998,6 +2001,10 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In first = false show_import_path(io, a, quote_level) end + elseif head === :as && nargs == 2 && valid_import_path(args[1], false) + show_import_path(io, args[1], quote_level) + print(io, " as ") + show_unquoted(io, args[2], indent, 0, quote_level) elseif head === :meta && nargs >= 2 && args[1] === :push_loc print(io, "# meta: location ", join(args[2:end], " ")) elseif head === :meta && nargs == 1 && args[1] === :pop_loc diff --git a/doc/src/manual/modules.md b/doc/src/manual/modules.md index 803000d151c5b..539f1a1001468 100644 --- a/doc/src/manual/modules.md +++ b/doc/src/manual/modules.md @@ -13,9 +13,9 @@ is shown for illustrative purposes: module MyModule using Lib -using BigLib: thing1, thing2 +using BigLib: thing1, thing2, thing3 as t3 -import Base.show +import Base.show, Base.print as pr export MyType, foo @@ -26,7 +26,7 @@ end bar(x) = 2x foo(a::MyType) = bar(a.x) + 1 -show(io::IO, a::MyType) = print(io, "MyType $(a.x)") +show(io::IO, a::MyType) = pr(io, "MyType $(a.x)") end ``` @@ -86,6 +86,37 @@ functions into the current workspace: | `import MyModule.x, MyModule.p` | `x` and `p` | `x` and `p` | | `import MyModule: x, p` | `x` and `p` | `x` and `p` | +### Import renaming + +An identifier brought into scope by `import` or `using` can be renamed with the keyword `as`. +This is useful for working around name conflicts as well as for shortening names. +For example, `Base` exports the function name `read`, but the CSV.jl package also provides `CSV.read`. +If we are going to invoke CSV reading many times, it would be convenient to drop the `CSV.` qualifier. +But then it is ambiguous whether we are referring to `Base.read` or `CSV.read`: + +```julia +julia> read; + +julia> import CSV.read +WARNING: ignoring conflicting import of CSV.read into Main +``` + +Renaming provides a solution: + +```julia +julia> import CSV.read as rd +``` + +Imported packages themselves can also be renamed: + +```julia +import BenchmarkTools as BT +``` + +`as` works with `using` only when a single identifier is brought into scope. +For example `using CSV: read as rd` works, but `using CSV as C` does not, since it operates +on all of the exported names in `CSV`. + ### Modules and files Files and file names are mostly unrelated to modules; modules are associated only with module diff --git a/src/ast.c b/src/ast.c index fc7cdbbf5e787..cd26a1654134e 100644 --- a/src/ast.c +++ b/src/ast.c @@ -44,7 +44,7 @@ jl_sym_t *exc_sym; jl_sym_t *error_sym; jl_sym_t *new_sym; jl_sym_t *using_sym; jl_sym_t *splatnew_sym; jl_sym_t *const_sym; jl_sym_t *thunk_sym; -jl_sym_t *foreigncall_sym; +jl_sym_t *foreigncall_sym; jl_sym_t *as_sym; jl_sym_t *global_sym; jl_sym_t *list_sym; jl_sym_t *dot_sym; jl_sym_t *newvar_sym; jl_sym_t *boundscheck_sym; jl_sym_t *inbounds_sym; @@ -363,6 +363,7 @@ void jl_init_common_symbols(void) thunk_sym = jl_symbol("thunk"); toplevel_sym = jl_symbol("toplevel"); dot_sym = jl_symbol("."); + as_sym = jl_symbol("as"); colon_sym = jl_symbol(":"); boundscheck_sym = jl_symbol("boundscheck"); inbounds_sym = jl_symbol("inbounds"); diff --git a/src/ast.scm b/src/ast.scm index 4a1a5aca055de..6ed530718e3db 100644 --- a/src/ast.scm +++ b/src/ast.scm @@ -46,6 +46,21 @@ (string ":" (deparse e)) (deparse e))) +(define (deparse-import-path e) + (cond ((and (pair? e) (eq? (car e) '|.|)) + (let loop ((lst (cdr e)) + (ndots 0)) + (if (or (null? lst) + (not (eq? (car lst) '|.|))) + (string (string.rep "." ndots) + (string.join (map deparse lst) ".")) + (loop (cdr lst) (+ ndots 1))))) + ((and (pair? e) (eq? (car e) ':)) + (string (deparse-import-path (cadr e)) ": " + (string.join (map deparse-import-path (cddr e)) ", "))) + (else + (string e)))) + (define (deparse e (ilvl 0)) (cond ((or (symbol? e) (number? e)) (string e)) ((string? e) (print-to-string e)) @@ -71,6 +86,8 @@ (if (length= e 2) (string (car e) (deparse (cadr e))) (string (deparse (cadr e)) " " (car e) " " (deparse (caddr e))))) + ((eq? (car e) 'as) + (string (deparse-import-path (cadr e)) " as " (deparse (caddr e)))) (else (case (car e) ((null) "nothing") @@ -209,21 +226,7 @@ "end")) ;; misc syntax forms ((import using) - (define (deparse-path e) - (cond ((and (pair? e) (eq? (car e) '|.|)) - (let loop ((lst (cdr e)) - (ndots 0)) - (if (or (null? lst) - (not (eq? (car lst) '|.|))) - (string (string.rep "." ndots) - (string.join (map deparse lst) ".")) - (loop (cdr lst) (+ ndots 1))))) - ((and (pair? e) (eq? (car e) ':)) - (string (deparse-path (cadr e)) ": " - (string.join (map deparse-path (cddr e)) ", "))) - (else - (string e)))) - (string (car e) " " (string.join (map deparse-path (cdr e)) ", "))) + (string (car e) " " (string.join (map deparse-import-path (cdr e)) ", "))) ((global local export) (string (car e) " " (string.join (map deparse (cdr e)) ", "))) ((const) (string "const " (deparse (cadr e)))) ((top) (deparse (cadr e))) diff --git a/src/julia-parser.scm b/src/julia-parser.scm index c75a100fe5711..9104c11606f2f 100644 --- a/src/julia-parser.scm +++ b/src/julia-parser.scm @@ -1580,18 +1580,21 @@ e)) (define (parse-imports s word) - (let* ((first (parse-import s word)) + (let* ((first (parse-import s word #f)) (next (peek-token s)) - (from (and (eq? next ':) (not (ts:space? s)))) + (from (and (eq? next ':) (not (ts:space? s)) + (or (not (and (pair? first) (eq? (car first) 'as))) + (error (string "invalid syntax \"" word " " (deparse first) ":\""))))) (done (cond ((or from (eqv? next #\,)) (begin (take-token s) #f)) - ((or (eq? next '|.|) + ;; TODO: this seems to be wrong; figure out if it's needed + #;((or (eq? next '|.|) (eqv? (string.sub (string next) 0 1) ".")) #f) (else #t))) (rest (if done '() (let ((ex (parse-comma-separated s (lambda (s) - (parse-import s word))))) + (parse-import s word from))))) (if (eq? (peek-token s) ':) (error (string "\":\" in \"" word "\" syntax can only be used " "when importing a single module. " @@ -1599,7 +1602,9 @@ ex))))) (if from `(,word (|:| ,first ,@rest)) - (list* word first rest)))) + (begin (if (and (eq? word 'using) (pair? first) (eq? (car first) 'as)) + (error (string "invalid syntax \"using " (deparse-import-path (cadr first)) " as ...\""))) + (list* word first rest))))) (define (parse-import-dots s) (let loop ((l '()) @@ -1619,7 +1624,7 @@ (else (cons (macrocall-to-atsym (parse-unary-prefix s)) l))))) -(define (parse-import s word) +(define (parse-import-path s word) (let loop ((path (parse-import-dots s))) (if (not (symbol-or-interpolate? (car path))) (error (string "invalid \"" word "\" statement: expected identifier"))) @@ -1639,6 +1644,16 @@ (else (cons '|.| (reverse path))))))) +(define (parse-import s word from) + (let ((path (parse-import-path s word))) + (if (eq? (peek-token s) 'as) + (begin + (if (and (not from) (eq? word 'using)) + (error (string "invalid syntax \"using " (deparse-import-path path) " as ...\""))) + (take-token s) + `(as ,path ,(parse-unary-prefix s))) + path))) + ;; parse comma-separated assignments, like "i=1:n,j=1:m,..." (define (parse-comma-separated s what) (let loop ((exprs '())) diff --git a/src/julia.h b/src/julia.h index 3e5a813fcba32..bd3173b7cb79f 100644 --- a/src/julia.h +++ b/src/julia.h @@ -1474,8 +1474,9 @@ JL_DLLEXPORT void jl_checked_assignment(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_ JL_DLLEXPORT void jl_declare_constant(jl_binding_t *b); JL_DLLEXPORT void jl_module_using(jl_module_t *to, jl_module_t *from); JL_DLLEXPORT void jl_module_use(jl_module_t *to, jl_module_t *from, jl_sym_t *s); -JL_DLLEXPORT void jl_module_import(jl_module_t *to, jl_module_t *from, - jl_sym_t *s); +JL_DLLEXPORT void jl_module_use_as(jl_module_t *to, jl_module_t *from, jl_sym_t *s, jl_sym_t *asname); +JL_DLLEXPORT void jl_module_import(jl_module_t *to, jl_module_t *from, jl_sym_t *s); +JL_DLLEXPORT void jl_module_import_as(jl_module_t *to, jl_module_t *from, jl_sym_t *s, jl_sym_t *asname); JL_DLLEXPORT void jl_module_export(jl_module_t *from, jl_sym_t *s); JL_DLLEXPORT int jl_is_imported(jl_module_t *m, jl_sym_t *s); JL_DLLEXPORT int jl_module_exports_p(jl_module_t *m, jl_sym_t *var) JL_NOTSAFEPOINT; diff --git a/src/julia_internal.h b/src/julia_internal.h index c0ec8504df4e6..e56cbe1bf71ec 100644 --- a/src/julia_internal.h +++ b/src/julia_internal.h @@ -1244,7 +1244,7 @@ extern jl_sym_t *new_sym; extern jl_sym_t *using_sym; extern jl_sym_t *splatnew_sym; extern jl_sym_t *pop_exception_sym; extern jl_sym_t *const_sym; extern jl_sym_t *thunk_sym; -extern jl_sym_t *foreigncall_sym; +extern jl_sym_t *foreigncall_sym; extern jl_sym_t *as_sym; extern jl_sym_t *global_sym; extern jl_sym_t *list_sym; extern jl_sym_t *dot_sym; extern jl_sym_t *newvar_sym; extern jl_sym_t *boundscheck_sym; extern jl_sym_t *inbounds_sym; diff --git a/src/module.c b/src/module.c index 06c5662403042..55ddab8b17c26 100644 --- a/src/module.c +++ b/src/module.c @@ -224,14 +224,14 @@ JL_DLLEXPORT jl_binding_t *jl_get_binding_for_method_def(jl_module_t *m, jl_sym_ } else { JL_UNLOCK_NOGC(&m->lock); - jl_binding_t *b2 = jl_get_binding(b->owner, var); + jl_binding_t *b2 = jl_get_binding(b->owner, b->name); if (b2 == NULL || b2->value == NULL) jl_errorf("invalid method definition: imported function %s.%s does not exist", - jl_symbol_name(b->owner->name), jl_symbol_name(var)); + jl_symbol_name(b->owner->name), jl_symbol_name(b->name)); // TODO: we might want to require explicitly importing types to add constructors if (!b->imported && !jl_is_type(b2->value)) { jl_errorf("error in method definition: function %s.%s must be explicitly imported to be extended", - jl_symbol_name(b->owner->name), jl_symbol_name(var)); + jl_symbol_name(b->owner->name), jl_symbol_name(b->name)); } return b2; } @@ -248,7 +248,7 @@ JL_DLLEXPORT jl_binding_t *jl_get_binding_for_method_def(jl_module_t *m, jl_sym_ return b; } -static void module_import_(jl_module_t *to, jl_module_t *from, jl_sym_t *s, +static void module_import_(jl_module_t *to, jl_module_t *from, jl_sym_t *s, jl_sym_t *asname, int explici); typedef struct _modstack_t { @@ -331,14 +331,14 @@ static jl_binding_t *jl_get_binding_(jl_module_t *m, jl_sym_t *var, modstack_t * // do a full import to prevent the result of this lookup // from changing, for example if this var is assigned to // later. - module_import_(m, b->owner, var, 0); + module_import_(m, b->owner, var, var, 0); return b; } return NULL; } JL_UNLOCK(&m->lock); if (b->owner != m) - return jl_get_binding_(b->owner, var, &top); + return jl_get_binding_(b->owner, b->name, &top); return b; } @@ -404,7 +404,7 @@ JL_DLLEXPORT int jl_is_imported(jl_module_t *m, jl_sym_t *s) } // NOTE: we use explici since explicit is a C++ keyword -static void module_import_(jl_module_t *to, jl_module_t *from, jl_sym_t *s, int explici) +static void module_import_(jl_module_t *to, jl_module_t *from, jl_sym_t *s, jl_sym_t *asname, int explici) { jl_binding_t *b = jl_get_binding(from, s); if (b == NULL) { @@ -431,19 +431,27 @@ static void module_import_(jl_module_t *to, jl_module_t *from, jl_sym_t *s, int } JL_LOCK(&to->lock); - jl_binding_t **bp = (jl_binding_t**)ptrhash_bp(&to->bindings, s); + jl_binding_t **bp = (jl_binding_t**)ptrhash_bp(&to->bindings, asname); jl_binding_t *bto = *bp; if (bto != HT_NOTFOUND) { if (bto == b) { // importing a binding on top of itself. harmless. } + else if (bto->name != s) { + JL_UNLOCK(&to->lock); + jl_printf(JL_STDERR, + "WARNING: ignoring conflicting import of %s.%s into %s\n", + jl_symbol_name(from->name), jl_symbol_name(s), + jl_symbol_name(to->name)); + return; + } else if (bto->owner == b->owner) { // already imported bto->imported = (explici!=0); } else if (bto->owner != to && bto->owner != NULL) { // already imported from somewhere else - jl_binding_t *bval = jl_get_binding(to, s); + jl_binding_t *bval = jl_get_binding(to, asname); if (bval->constp && bval->value && b->constp && b->value == bval->value) { // equivalent binding bto->imported = (explici!=0); @@ -493,12 +501,22 @@ static void module_import_(jl_module_t *to, jl_module_t *from, jl_sym_t *s, int JL_DLLEXPORT void jl_module_import(jl_module_t *to, jl_module_t *from, jl_sym_t *s) { - module_import_(to, from, s, 1); + module_import_(to, from, s, s, 1); +} + +JL_DLLEXPORT void jl_module_import_as(jl_module_t *to, jl_module_t *from, jl_sym_t *s, jl_sym_t *asname) +{ + module_import_(to, from, s, asname, 1); } JL_DLLEXPORT void jl_module_use(jl_module_t *to, jl_module_t *from, jl_sym_t *s) { - module_import_(to, from, s, 0); + module_import_(to, from, s, s, 0); +} + +JL_DLLEXPORT void jl_module_use_as(jl_module_t *to, jl_module_t *from, jl_sym_t *s, jl_sym_t *asname) +{ + module_import_(to, from, s, asname, 0); } JL_DLLEXPORT void jl_module_using(jl_module_t *to, jl_module_t *from) diff --git a/src/toplevel.c b/src/toplevel.c index 5e2aeb5b1ef69..8ad264f0bfcb0 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -542,10 +542,10 @@ static jl_method_instance_t *method_instance_for_thunk(jl_code_info_t *src, jl_m return li; } -static void import_module(jl_module_t *JL_NONNULL m, jl_module_t *import) +static void import_module(jl_module_t *JL_NONNULL m, jl_module_t *import, jl_sym_t *asname) { assert(m); - jl_sym_t *name = import->name; + jl_sym_t *name = asname ? asname : import->name; jl_binding_t *b; if (jl_binding_resolved_p(m, name)) { b = jl_get_binding(m, name); @@ -691,13 +691,25 @@ jl_value_t *jl_toplevel_eval_flex(jl_module_t *JL_NONNULL m, jl_value_t *e, int if (m == jl_main_module && name == NULL) { // TODO: for now, `using A` in Main also creates an explicit binding for `A` // This will possibly be extended to all modules. - import_module(m, u); + import_module(m, u, NULL); } } + continue; } - else { - jl_eval_errorf(m, "syntax: malformed \"using\" statement"); + else if (from && jl_is_expr(a) && ((jl_expr_t*)a)->head == as_sym && jl_expr_nargs(a) == 2 && + jl_is_expr(jl_exprarg(a, 0)) && ((jl_expr_t*)jl_exprarg(a, 0))->head == dot_sym) { + jl_sym_t *asname = (jl_sym_t*)jl_exprarg(a, 1); + if (jl_is_symbol(asname)) { + jl_expr_t *path = (jl_expr_t*)jl_exprarg(a, 0); + name = NULL; + jl_module_t *import = eval_import_path(m, from, ((jl_expr_t*)path)->args, &name, "using"); + assert(name); + // `using A: B as C` syntax + jl_module_use_as(m, import, name, asname); + continue; + } } + jl_eval_errorf(m, "syntax: malformed \"using\" statement"); } JL_GC_POP(); return jl_nothing; @@ -716,15 +728,34 @@ jl_value_t *jl_toplevel_eval_flex(jl_module_t *JL_NONNULL m, jl_value_t *e, int name = NULL; jl_module_t *import = eval_import_path(m, from, ((jl_expr_t*)a)->args, &name, "import"); if (name == NULL) { - import_module(m, import); + // `import A` syntax + import_module(m, import, NULL); } else { + // `import A.B` or `import A: B` syntax jl_module_import(m, import, name); } + continue; } - else { - jl_eval_errorf(m, "syntax: malformed \"import\" statement"); + else if (jl_is_expr(a) && ((jl_expr_t*)a)->head == as_sym && jl_expr_nargs(a) == 2 && + jl_is_expr(jl_exprarg(a, 0)) && ((jl_expr_t*)jl_exprarg(a, 0))->head == dot_sym) { + jl_sym_t *asname = (jl_sym_t*)jl_exprarg(a, 1); + if (jl_is_symbol(asname)) { + jl_expr_t *path = (jl_expr_t*)jl_exprarg(a, 0); + name = NULL; + jl_module_t *import = eval_import_path(m, from, ((jl_expr_t*)path)->args, &name, "import"); + if (name == NULL) { + // `import A as B` syntax + import_module(m, import, asname); + } + else { + // `import A.B as C` syntax + jl_module_import_as(m, import, name, asname); + } + continue; + } } + jl_eval_errorf(m, "syntax: malformed \"import\" statement"); } JL_GC_POP(); return jl_nothing; diff --git a/test/show.jl b/test/show.jl index f49dab0d755fa..3945bef2215d5 100644 --- a/test/show.jl +++ b/test/show.jl @@ -205,6 +205,11 @@ end @test_repr "import A.B.C: a, x, y.z" @test_repr "import ..A: a, x, y.z" @test_repr "import A.B, C.D" +@test_repr "import A as B" +@test_repr "import A.x as y" +@test_repr "import A: x as y" +@test_repr "import A.B: x, y as z" +@test_repr "import A.B: x, y as z, a.b as c, xx" # keyword args (issue #34023 and #32775) @test_repr "f(a, b=c)" diff --git a/test/syntax.jl b/test/syntax.jl index 74c4522f6d774..c96860b55e0c3 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -2381,3 +2381,51 @@ struct B37890{A, B} B37890(a, b) = new{Int, ()..., Int8}(a, b) end @test B37890(1.0, 2.0f0) isa B37890{Int, Int8} + +# import ... as +@test_throws ParseError("invalid syntax \"using A as ...\"") Meta.parse("using A as B") +@test_throws ParseError("invalid syntax \"using A.b as ...\"") Meta.parse("using A.b as B") +@test_throws ParseError("invalid syntax \"using A.b as ...\"") Meta.parse("using X, A.b as B") +@test_throws ParseError("invalid syntax \"import A as B:\"") Meta.parse("import A as B: c") +@test_throws ParseError("invalid syntax \"import A.b as B:\"") Meta.parse("import A.b as B: c") + +module TestImportAs +using Test + +module Mod +const x = 1 +global maybe_undef +def() = (global maybe_undef = 0) +func(x) = 2x + 1 +end + +module Mod2 +const y = 2 +end + +import .Mod: x as x2 + +@test x2 == 1 +@test !@isdefined(x) + +import .Mod2.y as y2 + +@test y2 == 2 +@test !@isdefined(y) + +@test_throws ErrorException eval(:(import .Mod.x as (a.b))) + +import .Mod.maybe_undef as mu +@test_throws UndefVarError mu +Mod.def() +@test mu === 0 + +using .Mod: func as f +@test f(10) == 21 +@test !@isdefined(func) +@test_throws ErrorException("error in method definition: function Mod.func must be explicitly imported to be extended") eval(:(f(x::Int) = x)) +end + +import .TestImportAs.Mod2 as M2 +@test !@isdefined(Mod2) +@test M2 === TestImportAs.Mod2