From ebfb307d11db4699ad6f3a6c599a472b6e6330fc Mon Sep 17 00:00:00 2001 From: Jeff Bezanson Date: Thu, 1 Jun 2017 17:56:31 -0400 Subject: [PATCH] implement named tuples Based on #16580, also much work done by quinnj. `(a=1, ...)` syntax is implemented, and `(; ...)` syntax is implemented but not yet enabled. --- NEWS.md | 3 + base/boot.jl | 2 +- base/inference.jl | 17 ++- base/namedtuple.jl | 215 ++++++++++++++++++++++++++++++++++++ base/reflection.jl | 20 +++- base/sysimg.jl | 5 +- doc/src/manual/functions.md | 50 ++++++++- doc/src/manual/types.md | 25 +++++ src/ast.scm | 1 + src/builtins.c | 1 + src/datatype.c | 3 +- src/dump.c | 9 +- src/gf.c | 2 + src/interpreter.c | 2 +- src/jltypes.c | 77 ++++++++++--- src/julia-syntax.scm | 95 ++++++++++++++-- src/julia.h | 14 ++- src/rtutils.c | 6 +- src/staticdata.c | 2 + test/ambiguous.jl | 1 + test/choosetests.jl | 2 +- test/inference.jl | 16 +++ test/namedtuple.jl | 191 ++++++++++++++++++++++++++++++++ 23 files changed, 715 insertions(+), 44 deletions(-) create mode 100644 base/namedtuple.jl create mode 100644 test/namedtuple.jl diff --git a/NEWS.md b/NEWS.md index d2e14b873ed5c..48cb2f3ed15d8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,9 @@ New language features a function argument name, the argument is unpacked into local variables `x` and `y` as in the assignment `(x, y) = arg` ([#6614]). + * Named tuples, with the syntax `(a=1, b=2)`. These behave very similarly to tuples, + except components can also be accessed by name using dot syntax `t.a` ([#22194]). + * Custom infix operators can now be defined by appending Unicode combining marks, primes, and sub/superscripts to other operators. For example, `+̂ₐ″` is parsed as an infix operator with the same diff --git a/base/boot.jl b/base/boot.jl index 7d1163f24d32d..ae4d02e553d09 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -120,7 +120,7 @@ export # key types Any, DataType, Vararg, ANY, NTuple, Tuple, Type, UnionAll, TypeName, TypeVar, Union, Void, - SimpleVector, AbstractArray, DenseArray, + SimpleVector, AbstractArray, DenseArray, NamedTuple, # special objects Function, CodeInfo, Method, MethodTable, TypeMapEntry, TypeMapLevel, Module, Symbol, Task, Array, WeakRef, VecElement, diff --git a/base/inference.jl b/base/inference.jl index 53585a3288371..865cd4a63d5bc 100644 --- a/base/inference.jl +++ b/base/inference.jl @@ -493,6 +493,8 @@ end const _Type_name = Type.body.name isType(@nospecialize t) = isa(t, DataType) && (t::DataType).name === _Type_name +const _NamedTuple_name = NamedTuple.body.body.name + # true if Type is inlineable as constant (is a singleton) function isconstType(@nospecialize t) isType(t) || return false @@ -734,6 +736,10 @@ function isdefined_tfunc(args...) end if 1 <= idx <= a1.ninitialized return Const(true) + elseif a1.name === _NamedTuple_name + if isleaftype(a1) + return Const(false) + end elseif idx <= 0 || (!isvatuple(a1) && idx > fieldcount(a1)) return Const(false) elseif !isvatuple(a1) && isbits(fieldtype(a1, idx)) @@ -771,7 +777,9 @@ add_tfunc(nfields, 1, 1, # TODO: remove with deprecation in builtins.c for nfields(::Type) isleaftype(x.parameters[1]) && return Const(old_nfields(x.parameters[1])) elseif isa(x,DataType) && !x.abstract && !(x.name === Tuple.name && isvatuple(x)) && x !== DataType - return Const(length(x.types)) + if !(x.name === _NamedTuple_name && !isleaftype(x)) + return Const(length(x.types)) + end end return Int end, 0) @@ -1333,6 +1341,10 @@ function getfield_tfunc(@nospecialize(s00), @nospecialize(name)) end return Any end + if s.name === _NamedTuple_name && !isleaftype(s) + # TODO: better approximate inference + return Any + end if isempty(s.types) return Bottom end @@ -1416,6 +1428,9 @@ function fieldtype_tfunc(@nospecialize(s0), @nospecialize(name)) if !isa(u,DataType) || u.abstract return Type end + if u.name === _NamedTuple_name && !isleaftype(u) + return Type + end ftypes = u.types if isempty(ftypes) return Bottom diff --git a/base/namedtuple.jl b/base/namedtuple.jl new file mode 100644 index 0000000000000..4f224bf47afcf --- /dev/null +++ b/base/namedtuple.jl @@ -0,0 +1,215 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +""" + NamedTuple{names,T}(args::Tuple) + +Construct a named tuple with the given `names` (a tuple of Symbols) and field types `T` +(a `Tuple` type) from a tuple of values. +""" +function NamedTuple{names,T}(args::Tuple) where {names, T <: Tuple} + if length(args) == length(names) + if @generated + N = length(names) + types = T.parameters + Expr(:new, :(NamedTuple{names,T}), Any[ :(convert($(types[i]), args[$i])) for i in 1:N ]...) + else + N = length(names) + NT = NamedTuple{names,T} + types = T.parameters + fields = Any[ convert(types[i], args[i]) for i = 1:N ] + ccall(:jl_new_structv, Any, (Any, Ptr{Void}, UInt32), NT, fields, N)::NT + end + else + throw(ArgumentError("Wrong number of arguments to named tuple constructor.")) + end +end + +""" + NamedTuple{names}(args::Tuple) + +Construct a named tuple with the given `names` (a tuple of Symbols) from a tuple of +values. +""" +function NamedTuple{names}(args::Tuple) where {names} + NamedTuple{names,typeof(args)}(args) +end + +""" + NamedTuple{names}(nt::NamedTuple) + +Construct a named tuple by selecting fields in `names` (a tuple of Symbols) from +another named tuple. +""" +function NamedTuple{names}(nt::NamedTuple) where {names} + if @generated + types = Tuple{(fieldtype(nt, n) for n in names)...} + Expr(:new, :(NamedTuple{names, $types}), Any[ :(getfield(nt, $(QuoteNode(n)))) for n in names ]...) + else + types = Tuple{(fieldtype(typeof(nt), n) for n in names)...} + NamedTuple{names, types}(Tuple(getfield(nt, n) for n in names)) + end +end + +NamedTuple() = NamedTuple{(),Tuple{}}(()) + +length(t::NamedTuple) = nfields(t) +start(t::NamedTuple) = 1 +done(t::NamedTuple, iter) = iter > nfields(t) +next(t::NamedTuple, iter) = (getfield(t, iter), iter + 1) +endof(t::NamedTuple) = nfields(t) +getindex(t::NamedTuple, i::Int) = getfield(t, i) +getindex(t::NamedTuple, i::Symbol) = getfield(t, i) +indexed_next(t::NamedTuple, i::Int, state) = (getfield(t, i), i+1) + +convert(::Type{NamedTuple{names,T}}, nt::NamedTuple{names,T}) where {names,T} = nt +convert(::Type{NamedTuple{names}}, nt::NamedTuple{names}) where {names} = nt + +function convert(::Type{NamedTuple{names,T}}, nt::NamedTuple{names}) where {names,T} + NamedTuple{names,T}(T(nt)) +end + +function show(io::IO, t::NamedTuple) + n = nfields(t) + for i = 1:n + # if field types aren't concrete, show full type + if typeof(getfield(t, i)) !== fieldtype(typeof(t), i) + show(io, typeof(t)) + print(io, "(") + show(io, Tuple(t)) + print(io, ")") + return + end + end + if n == 0 + print(io, "NamedTuple()") + else + print(io, "(") + for i = 1:n + print(io, fieldname(typeof(t),i), " = "); show(io, getfield(t, i)) + if n == 1 + print(io, ",") + elseif i < n + print(io, ", ") + end + end + print(io, ")") + end +end + +eltype(::Type{NamedTuple{names,T}}) where {names,T} = eltype(T) + +==(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = Tuple(a) == Tuple(b) +==(a::NamedTuple, b::NamedTuple) = false + +isequal(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = isequal(Tuple(a), Tuple(b)) +isequal(a::NamedTuple, b::NamedTuple) = false + +_nt_names(::NamedTuple{names}) where {names} = names +_nt_names(::Type{T}) where {names,T<:NamedTuple{names}} = names + +hash(x::NamedTuple, h::UInt) = xor(object_id(_nt_names(x)), hash(Tuple(x), h)) + +isless(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = isless(Tuple(a), Tuple(b)) +# TODO: case where one argument's names are a prefix of the other's + +same_names(::NamedTuple{names}...) where {names} = true +same_names(::NamedTuple...) = false + +function map(f, nt::NamedTuple{names}, nts::NamedTuple...) where names + if !same_names(nt, nts...) + throw(ArgumentError("Named tuple names do not match.")) + end + # this method makes sure we don't define a map(f) method + NT = NamedTuple{names} + if @generated + N = length(names) + M = length(nts) + args = Expr[:(f($(Expr[:(getfield(nt, $j)), (:(getfield(nts[$i], $j)) for i = 1:M)...]...))) for j = 1:N] + :( NT(($(args...),)) ) + else + NT(map(f, map(Tuple, (nt, nts...))...)) + end +end + +# a version of `in` for the older world these generated functions run in +@pure function sym_in(x::Symbol, itr::Tuple{Vararg{Symbol}}) + for y in itr + y === x && return true + end + return false +end + +@pure function merge_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}}) + names = Symbol[an...] + for n in bn + if !sym_in(n, an) + push!(names, n) + end + end + (names...,) +end + +@pure function merge_types(names::Tuple{Vararg{Symbol}}, a::Type{<:NamedTuple}, b::Type{<:NamedTuple}) + bn = _nt_names(b) + Tuple{Any[ fieldtype(sym_in(n, bn) ? b : a, n) for n in names ]...} +end + +""" + merge(a::NamedTuple, b::NamedTuple) + +Construct a new named tuple by merging two existing ones. +The order of fields in `a` is preserved, but values are taken from matching +fields in `b`. Fields present only in `b` are appended at the end. + +```jldoctest +julia> merge((a=1, b=2, c=3), (b=4, d=5)) +(a = 1, b = 4, c = 3, d = 5) +``` +""" +function merge(a::NamedTuple{an}, b::NamedTuple{bn}) where {an, bn} + if @generated + names = merge_names(an, bn) + types = merge_types(names, a, b) + vals = Any[ :(getfield($(sym_in(n, bn) ? :b : :a), $(QuoteNode(n)))) for n in names ] + :( NamedTuple{$names,$types}(($(vals...),)) ) + else + names = merge_names(an, bn) + types = merge_types(names, typeof(a), typeof(b)) + NamedTuple{names,types}(map(n->getfield(sym_in(n, bn) ? b : a, n), names)) + end +end + +merge(a::NamedTuple{()}, b::NamedTuple) = b + +""" + merge(a::NamedTuple, iterable) + +Interpret an iterable of key-value pairs as a named tuple, and perform a merge. + +```jldoctest +julia> merge((a=1, b=2, c=3), [:b=>4, :d=>5]) +(a = 1, b = 4, c = 3, d = 5) +``` +""" +function merge(a::NamedTuple, itr) + names = Symbol[] + vals = Any[] + inds = ObjectIdDict() + for (k,v) in itr + oldind = get(inds, k, 0) + if oldind > 0 + vals[oldind] = v + else + push!(names, k) + push!(vals, v) + inds[k] = length(names) + end + end + merge(a, NamedTuple{(names...,)}((vals...,))) +end + +keys(nt::NamedTuple{names}) where {names} = names +values(nt::NamedTuple) = Tuple(nt) +haskey(nt::NamedTuple, key::Union{Integer, Symbol}) = isdefined(nt, key) +get(nt::NamedTuple, key::Union{Integer, Symbol}, default) = haskey(nt, key) ? getfield(nt, key) : default +get(f::Callable, nt::NamedTuple, key::Union{Integer, Symbol}) = haskey(nt, key) ? getfield(nt, key) : f() diff --git a/base/reflection.jl b/base/reflection.jl index 27a24eb540fd0..b7cf6c4ffbf55 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -127,12 +127,14 @@ julia> fieldname(SparseMatrixCSC, 5) ``` """ function fieldname(t::DataType, i::Integer) - n_fields = length(t.name.names) + names = isdefined(t, :names) ? t.names : t.name.names + n_fields = length(names) field_label = n_fields == 1 ? "field" : "fields" i > n_fields && throw(ArgumentError("Cannot access field $i since type $t only has $n_fields $field_label.")) i < 1 && throw(ArgumentError("Field numbers must be positive integers. $i is invalid.")) - return t.name.names[i]::Symbol + return names[i]::Symbol end + fieldname(t::UnionAll, i::Integer) = fieldname(unwrap_unionall(t), i) fieldname(t::Type{<:Tuple}, i::Integer) = i < 1 || i > fieldcount(t) ? throw(BoundsError(t, i)) : Int(i) @@ -481,7 +483,19 @@ function fieldcount(@nospecialize t) if !(t isa DataType) throw(TypeError(:fieldcount, "", Type, t)) end - if t.abstract || (t.name === Tuple.name && isvatuple(t)) + if t.name === NamedTuple.body.body.name + names, types = t.parameters + if names isa Tuple + return length(names) + end + if types isa DataType && types <: Tuple + return fieldcount(types) + end + abstr = true + else + abstr = t.abstract || (t.name === Tuple.name && isvatuple(t)) + end + if abstr error("type does not have a definite number of fields") end return length(t.types) diff --git a/base/sysimg.jl b/base/sysimg.jl index a0ec603619702..0a3963d4872a2 100644 --- a/base/sysimg.jl +++ b/base/sysimg.jl @@ -142,6 +142,10 @@ Vector(m::Integer) = Array{Any,1}(Int(m)) Matrix{T}(m::Integer, n::Integer) where {T} = Matrix{T}(Int(m), Int(n)) Matrix(m::Integer, n::Integer) = Matrix{Any}(Int(m), Int(n)) +include("associative.jl") + +include("namedtuple.jl") + # numeric operations include("hashing.jl") include("rounding.jl") @@ -175,7 +179,6 @@ include("reduce.jl") include("reshapedarray.jl") include("bitarray.jl") include("bitset.jl") -include("associative.jl") if !isdefined(Core, :Inference) include("docs/core.jl") diff --git a/doc/src/manual/functions.md b/doc/src/manual/functions.md index 3b1478c4dd659..2779d27c8f9a0 100644 --- a/doc/src/manual/functions.md +++ b/doc/src/manual/functions.md @@ -221,6 +221,48 @@ A zero-argument anonymous function is written as `()->3`. The idea of a function may seem strange, but is useful for "delaying" a computation. In this usage, a block of code is wrapped in a zero-argument function, which is later invoked by calling it as `f`. +## Tuples + +Julia has a built-in data structure called a *tuple* that is closely related to function +arguments and return values. +A tuple is a fixed-length container that can hold any values, but cannot be modified +(it is *immutable*). +Tuples are constructed with commas and parentheses, and can be accessed via indexing: + +```jldoctest +julia> (1, 1+1) +(1, 2) + +julia> (1,) +(1,) + +julia> x = (0.0, "hello", 6*7) +(0.0, "hello", 42) + +julia> x[2] +"hello" +``` + +Notice that a length-1 tuple must be written with a comma, `(1,)`, since `(1)` would just +be a parenthesized value. +`()` represents the empty (length-0) tuple. + +## Named Tuples + +The components of tuples can optionally be named, in which case a *named tuple* is +constructed: + +```jldoctest +julia> x = (a=1, b=1+1) +(a = 1, b = 2) + +julia> x.a +1 +``` + +Named tuples are very similar to tuples, except that fields can additionally be accessed by name +using dot syntax (`x.a`). + ## Multiple Return Values In Julia, one returns a tuple of values to simulate returning multiple values. However, tuples @@ -320,7 +362,7 @@ In all these cases, `x` is bound to a tuple of the trailing values passed to `ba It is possible to constrain the number of values passed as a variable argument; this will be discussed later in [Parametrically-constrained Varargs methods](@ref). -On the flip side, it is often handy to "splice" the values contained in an iterable collection +On the flip side, it is often handy to "splat" the values contained in an iterable collection into a function call as individual arguments. To do this, one also uses `...` but in the function call instead: @@ -349,7 +391,7 @@ julia> bar(x...) (1, 2, (3, 4)) ``` -Furthermore, the iterable object spliced into a function call need not be a tuple: +Furthermore, the iterable object splatted into a function call need not be a tuple: ```jldoctest barfunc julia> x = [3,4] @@ -371,7 +413,7 @@ julia> bar(x...) (1, 2, (3, 4)) ``` -Also, the function that arguments are spliced into need not be a varargs function (although it +Also, the function that arguments are splatted into need not be a varargs function (although it often is): ```jldoctest @@ -397,7 +439,7 @@ Closest candidates are: baz(::Any, ::Any) at none:1 ``` -As you can see, if the wrong number of elements are in the spliced container, then the function +As you can see, if the wrong number of elements are in the splatted container, then the function call will fail, just as it would if too many arguments were given explicitly. ## Optional Arguments diff --git a/doc/src/manual/types.md b/doc/src/manual/types.md index ef2955262bd1a..9eacec5057752 100644 --- a/doc/src/manual/types.md +++ b/doc/src/manual/types.md @@ -905,6 +905,31 @@ used to represent the arguments accepted by varargs methods (see [Varargs Functi The type `Vararg{T,N}` corresponds to exactly `N` elements of type `T`. `NTuple{N,T}` is a convenient alias for `Tuple{Vararg{T,N}}`, i.e. a tuple type containing exactly `N` elements of type `T`. +### Named Tuple Types + +Named tuples are instances of the `NamedTuple` type, which has two parameters: a tuple of +symbols giving the field names, and a tuple type giving the field types. + +```jldoctest +julia> typeof((a=1,b="hello")) +NamedTuple{(:a, :b),Tuple{Int64,String}} +``` + +A `NamedTuple` type can be used as a constructor, accepting a single tuple argument. +The constructed `NamedTuple` type can be either a concrete type, with both parameters specified, +or a type that specifies only field names: + +```jldoctest +julia> NamedTuple{(:a, :b),Tuple{Float32, String}}((1,"")) +(a = 1.0f0, b = "") + +julia> NamedTuple{(:a, :b)}((1,"")) +(a = 1, b = "") +``` + +If field types are specified, the arguments are converted. Otherwise the types of the arguments +are used directly. + #### [Singleton Types](@id man-singleton-types) There is a special kind of abstract parametric type that must be mentioned here: singleton types. diff --git a/src/ast.scm b/src/ast.scm index c8799364a93e2..5f724c34d3998 100644 --- a/src/ast.scm +++ b/src/ast.scm @@ -153,6 +153,7 @@ ;; predicates and accessors (define (quoted? e) (memq (car e) '(quote top core globalref outerref line break inert meta))) +(define (quotify e) `',e) (define (lam:args x) (cadr x)) (define (lam:vars x) (llist-vars (lam:args x))) diff --git a/src/builtins.c b/src/builtins.c index fa61ab54eadd6..38ae82d232cc1 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -1295,6 +1295,7 @@ void jl_init_primitives(void) add_builtin("QuoteNode", (jl_value_t*)jl_quotenode_type); add_builtin("NewvarNode", (jl_value_t*)jl_newvarnode_type); add_builtin("GlobalRef", (jl_value_t*)jl_globalref_type); + add_builtin("NamedTuple", (jl_value_t*)jl_namedtuple_type); add_builtin("Bool", (jl_value_t*)jl_bool_type); add_builtin("UInt8", (jl_value_t*)jl_uint8_type); diff --git a/src/datatype.c b/src/datatype.c index f413596592675..9ebc3a6d0ac15 100644 --- a/src/datatype.c +++ b/src/datatype.c @@ -81,6 +81,7 @@ jl_datatype_t *jl_new_uninitialized_datatype(void) t->hasfreetypevars = 0; t->isleaftype = 1; t->layout = NULL; + t->names = NULL; return t; } @@ -288,7 +289,7 @@ void jl_compute_field_offsets(jl_datatype_t *st) return; } } - if (st->types == NULL) + if (st->types == NULL || (jl_is_namedtuple_type(st) && !jl_is_leaf_type((jl_value_t*)st))) return; uint32_t nfields = jl_svec_len(st->types); if (nfields == 0) { diff --git a/src/dump.c b/src/dump.c index 4b24da41e7131..43beebcab1785 100644 --- a/src/dump.c +++ b/src/dump.c @@ -367,6 +367,7 @@ static void jl_serialize_datatype(jl_serializer_state *s, jl_datatype_t *dt) if (has_instance) jl_serialize_value(s, dt->instance); jl_serialize_value(s, dt->name); + jl_serialize_value(s, dt->names); jl_serialize_value(s, dt->parameters); jl_serialize_value(s, dt->super); jl_serialize_value(s, dt->types); @@ -1245,6 +1246,8 @@ static jl_value_t *jl_deserialize_datatype(jl_serializer_state *s, int pos, jl_v } dt->name = (jl_typename_t*)jl_deserialize_value(s, (jl_value_t**)&dt->name); jl_gc_wb(dt, dt->name); + dt->names = (jl_svec_t*)jl_deserialize_value(s, (jl_value_t**)&dt->names); + jl_gc_wb(dt, dt->names); dt->parameters = (jl_svec_t*)jl_deserialize_value(s, (jl_value_t**)&dt->parameters); jl_gc_wb(dt, dt->parameters); dt->super = (jl_datatype_t*)jl_deserialize_value(s, (jl_value_t**)&dt->super); @@ -2804,7 +2807,6 @@ void jl_init_serializer(void) jl_box_int32(30), jl_box_int32(31), jl_box_int32(32), #ifndef _P64 jl_box_int32(33), jl_box_int32(34), jl_box_int32(35), - jl_box_int32(36), jl_box_int32(37), #endif jl_box_int64(0), jl_box_int64(1), jl_box_int64(2), jl_box_int64(3), jl_box_int64(4), jl_box_int64(5), @@ -2819,7 +2821,6 @@ void jl_init_serializer(void) jl_box_int64(30), jl_box_int64(31), jl_box_int64(32), #ifdef _P64 jl_box_int64(33), jl_box_int64(34), jl_box_int64(35), - jl_box_int64(36), jl_box_int64(37), #endif jl_labelnode_type, jl_linenumbernode_type, jl_gotonode_type, jl_quotenode_type, jl_type_type, jl_bottom_type, jl_ref_type, @@ -2845,7 +2846,8 @@ void jl_init_serializer(void) jl_intrinsic_type->name, jl_task_type->name, jl_labelnode_type->name, jl_linenumbernode_type->name, jl_builtin_type->name, jl_gotonode_type->name, jl_quotenode_type->name, jl_globalref_type->name, jl_typeofbottom_type->name, - jl_string_type->name, jl_abstractstring_type->name, + jl_string_type->name, jl_abstractstring_type->name, jl_namedtuple_type, + jl_namedtuple_typename, ptls->root_task, @@ -2883,6 +2885,7 @@ void jl_init_serializer(void) arraylist_push(&builtin_typenames, ((jl_datatype_t*)jl_unwrap_unionall((jl_value_t*)jl_densearray_type))->name); arraylist_push(&builtin_typenames, jl_tuple_typename); arraylist_push(&builtin_typenames, jl_vararg_typename); + arraylist_push(&builtin_typenames, jl_namedtuple_typename); } #ifdef __cplusplus diff --git a/src/gf.c b/src/gf.c index 73cc18b47615f..a8603dbe976c0 100644 --- a/src/gf.c +++ b/src/gf.c @@ -2398,6 +2398,8 @@ int jl_has_concrete_subtype(jl_value_t *typ) typ = jl_unwrap_vararg(typ); if (!jl_is_datatype(typ)) return 1; + if (((jl_datatype_t*)typ)->name == jl_namedtuple_typename) + return jl_has_concrete_subtype(jl_tparam1(typ)); jl_svec_t *fields = ((jl_datatype_t*)typ)->types; size_t i, l = jl_svec_len(fields); if (l != ((jl_datatype_t*)typ)->ninitialized) diff --git a/src/interpreter.c b/src/interpreter.c index 8c2e1d0e521b0..12fad826c6b72 100644 --- a/src/interpreter.c +++ b/src/interpreter.c @@ -172,7 +172,7 @@ void jl_set_datatype_super(jl_datatype_t *tt, jl_value_t *super) if (!jl_is_datatype(super) || !jl_is_abstracttype(super) || tt->name == ((jl_datatype_t*)super)->name || jl_subtype(super,(jl_value_t*)jl_vararg_type) || - jl_is_tuple_type(super) || + jl_is_tuple_type(super) || jl_is_namedtuple_type(super) || jl_subtype(super,(jl_value_t*)jl_type_type) || super == (jl_value_t*)jl_builtin_type) { jl_errorf("invalid subtyping in definition of %s", diff --git a/src/jltypes.c b/src/jltypes.c index 56209a26aedef..09d44e8f9dc8a 100644 --- a/src/jltypes.c +++ b/src/jltypes.c @@ -112,6 +112,8 @@ jl_unionall_t *jl_pointer_type; jl_typename_t *jl_pointer_typename; jl_datatype_t *jl_void_type; jl_datatype_t *jl_voidpointer_type; +jl_typename_t *jl_namedtuple_typename; +jl_unionall_t *jl_namedtuple_type; jl_value_t *jl_an_empty_vec_any=NULL; jl_value_t *jl_stackovf_exception; #ifdef SEGV_EXCEPTION @@ -1133,6 +1135,7 @@ static jl_value_t *inst_datatype_inner(jl_datatype_t *dt, jl_svec_t *p, jl_value jl_typestack_t top; jl_typename_t *tn = dt->name; int istuple = (tn == jl_tuple_typename); + int isnamedtuple = (tn == jl_namedtuple_typename); // check type cache if (cacheable) { JL_LOCK(&typecache_lock); // Might GC @@ -1255,7 +1258,41 @@ static jl_value_t *inst_datatype_inner(jl_datatype_t *dt, jl_svec_t *p, jl_value ndt->super = NULL; ndt->parameters = p; jl_gc_wb(ndt, ndt->parameters); - ndt->types = istuple ? p : NULL; // to be filled in below + ndt->types = NULL; // to be filled in below + if (istuple) { + ndt->types = p; + } + else if (isnamedtuple) { + jl_value_t *names_tup = jl_svecref(p, 0); + jl_value_t *values_tt = jl_svecref(p, 1); + if (!jl_has_free_typevars(names_tup) && !jl_has_free_typevars(values_tt)) { + if (!jl_is_tuple(names_tup)) + jl_type_error_rt("NamedTuple", "names", (jl_value_t*)jl_anytuple_type, names_tup); + size_t nf = jl_nfields(names_tup); + jl_svec_t *names = jl_alloc_svec_uninit(nf); + for (size_t i = 0; i < nf; i++) { + jl_value_t *ni = jl_fieldref(names_tup, i); + if (!jl_is_symbol(ni)) + jl_type_error_rt("NamedTuple", "name", (jl_value_t*)jl_symbol_type, ni); + for (size_t j = 0; j < i; j++) { + if (ni == jl_svecref(names, j)) + jl_errorf("duplicate field name in NamedTuple: \"%s\" is not unique", jl_symbol_name((jl_sym_t*)ni)); + } + jl_svecset(names, i, ni); + } + if (!jl_is_datatype(values_tt)) + jl_error("NamedTuple field type must be a tuple type"); + if (jl_is_va_tuple((jl_datatype_t*)values_tt) || jl_nparams(values_tt) != nf) + jl_error("NamedTuple names and field types must have matching lengths"); + ndt->names = names; + jl_gc_wb(ndt, ndt->names); + ndt->types = ((jl_datatype_t*)values_tt)->parameters; + jl_gc_wb(ndt, ndt->types); + } + else { + ndt->types = jl_emptysvec; + } + } ndt->mutabl = dt->mutabl; ndt->abstract = dt->abstract; ndt->instance = NULL; @@ -1269,7 +1306,7 @@ static jl_value_t *inst_datatype_inner(jl_datatype_t *dt, jl_svec_t *p, jl_value if (cacheable && !ndt->abstract) ndt->uid = jl_assign_type_uid(); - if (istuple) { + if (istuple || isnamedtuple) { ndt->super = jl_any_type; } else if (dt->super) { @@ -1280,13 +1317,13 @@ static jl_value_t *inst_datatype_inner(jl_datatype_t *dt, jl_svec_t *p, jl_value if (ftypes == NULL || dt->super == NULL) { // in the process of creating this type definition: // need to instantiate the super and types fields later - assert(inside_typedef && !istuple); + assert(inside_typedef && !istuple && !isnamedtuple); arraylist_push(&partial_inst, ndt); } else { - assert(ftypes != jl_emptysvec || jl_field_names(ndt) == jl_emptysvec); + assert(ftypes != jl_emptysvec || jl_field_names(ndt) == jl_emptysvec || isnamedtuple); assert(ftypes == jl_emptysvec || !ndt->abstract); - if (!istuple) { + if (!istuple && !isnamedtuple) { // recursively instantiate the types of the fields ndt->types = inst_all(ftypes, env, stack, 1); jl_gc_wb(ndt, ndt->types); @@ -1302,6 +1339,8 @@ static jl_value_t *inst_datatype_inner(jl_datatype_t *dt, jl_svec_t *p, jl_value if (istuple) ndt->ninitialized = ntp - isvatuple; + else if (isnamedtuple) + ndt->ninitialized = jl_svec_len(ndt->types); else ndt->ninitialized = dt->ninitialized; @@ -1687,11 +1726,12 @@ void jl_init_types(void) jl_datatype_type->name->wrapper = (jl_value_t*)jl_datatype_type; jl_datatype_type->super = (jl_datatype_t*)jl_type_type; jl_datatype_type->parameters = jl_emptysvec; - jl_datatype_type->name->names = jl_perm_symsvec(16, + jl_datatype_type->name->names = jl_perm_symsvec(17, "name", "super", "parameters", "types", + "names", "instance", "layout", "size", @@ -1704,11 +1744,11 @@ void jl_init_types(void) "depth", "hasfreetypevars", "isleaftype"); - jl_datatype_type->types = jl_svec(16, + jl_datatype_type->types = jl_svec(17, jl_typename_type, jl_datatype_type, jl_simplevector_type, - jl_simplevector_type, + jl_simplevector_type, jl_simplevector_type, jl_any_type, // instance jl_any_type, jl_any_type, jl_any_type, jl_any_type, jl_any_type, jl_any_type, jl_any_type, jl_any_type, @@ -2147,20 +2187,29 @@ void jl_init_types(void) jl_string_type->instance = NULL; jl_compute_field_offsets(jl_string_type); + jl_tvar_t *ntval_var = jl_new_typevar(jl_symbol("T"), (jl_value_t*)jl_bottom_type, + (jl_value_t*)jl_anytuple_type); + tv = jl_svec2(tvar("names"), ntval_var); + jl_datatype_t *ntt = jl_new_datatype(jl_symbol("NamedTuple"), core, jl_any_type, tv, + jl_emptysvec, jl_emptysvec, 0, 0, 0); + jl_namedtuple_type = (jl_unionall_t*)ntt->name->wrapper; + ((jl_datatype_t*)jl_unwrap_unionall((jl_value_t*)jl_namedtuple_type))->layout = NULL; + jl_namedtuple_typename = ntt->name; + // complete builtin type metadata jl_value_t *pointer_void = jl_apply_type1((jl_value_t*)jl_pointer_type, (jl_value_t*)jl_void_type); jl_voidpointer_type = (jl_datatype_t*)pointer_void; - jl_svecset(jl_datatype_type->types, 5, jl_voidpointer_type); - jl_svecset(jl_datatype_type->types, 6, jl_int32_type); + jl_svecset(jl_datatype_type->types, 6, jl_voidpointer_type); jl_svecset(jl_datatype_type->types, 7, jl_int32_type); jl_svecset(jl_datatype_type->types, 8, jl_int32_type); - jl_svecset(jl_datatype_type->types, 9, jl_bool_type); + jl_svecset(jl_datatype_type->types, 9, jl_int32_type); jl_svecset(jl_datatype_type->types, 10, jl_bool_type); - jl_svecset(jl_datatype_type->types, 11, jl_voidpointer_type); + jl_svecset(jl_datatype_type->types, 11, jl_bool_type); jl_svecset(jl_datatype_type->types, 12, jl_voidpointer_type); - jl_svecset(jl_datatype_type->types, 13, jl_int32_type); - jl_svecset(jl_datatype_type->types, 14, jl_bool_type); + jl_svecset(jl_datatype_type->types, 13, jl_voidpointer_type); + jl_svecset(jl_datatype_type->types, 14, jl_int32_type); jl_svecset(jl_datatype_type->types, 15, jl_bool_type); + jl_svecset(jl_datatype_type->types, 16, jl_bool_type); jl_svecset(jl_typename_type->types, 1, jl_module_type); jl_svecset(jl_typename_type->types, 6, jl_long_type); jl_svecset(jl_typename_type->types, 3, jl_type_type); diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index 6286affbb2c87..9741051a77836 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -924,7 +924,7 @@ ,@(map (lambda (v) `(local ,v)) params) ,@(map (lambda (n v) (make-assignment n (bounds-to-TypeVar v))) params bounds) (struct_type ,name (call (core svec) ,@params) - (call (core svec) ,@(map (lambda (x) `',x) field-names)) + (call (core svec) ,@(map quotify field-names)) ,super (call (core svec) ,@field-types) ,mut ,min-initialized))) ;; "inner" constructors (scope-block @@ -1920,6 +1920,76 @@ (extract (cdr params) (cons p newparams) whereparams))))) (extract (cddr e) '() '())) +(define (named-tuple-expr names values) + `(call (curly (core NamedTuple) (tuple ,@names)) + (tuple ,@values))) + +(define (lower-named-tuple lst) + (let* ((names (apply append + (map (lambda (x) + (cond #;((symbol? x) (list x)) + ((and (or (assignment? x) (kwarg? x)) (symbol? (cadr x))) + (list (cadr x))) + #;((and (length= x 3) (eq? (car x) '|.|)) + (list (cadr (caddr x)))) + (else '()))) + lst))) + (dups (has-dups names))) + (if dups + (error (string "field name \"" (car dups) "\" repeated in named tuple")))) + (define (to-nt n v) + (if (null? n) + #f + (named-tuple-expr (reverse! (map quotify n)) (reverse v)))) + (define (merge old new) + (if old + (if new + `(call (top merge) ,old ,new) + old) + new)) + (let loop ((L lst) + (current-names '()) + (current-vals '()) + (expr #f)) + (if (null? L) + (merge expr (to-nt current-names current-vals)) + (let ((el (car L))) + (cond ((or (assignment? el) (kwarg? el)) + (if (not (symbol? (cadr el))) + (error (string "invalid named tuple field name \"" (deparse (cadr el)) "\""))) + (loop (cdr L) + (cons (cadr el) current-names) + (cons (caddr el) current-vals) + expr)) +#| + ((symbol? el) ;; x => x = x + (loop (cdr L) + (cons el current-names) + (cons el current-vals) + expr)) + ((and (length= el 3) (eq? (car el) '|.|)) ;; a.x => x = a.x + (loop (cdr L) + (cons (cadr (caddr el)) current-names) + (cons el current-vals) + expr)) + ((and (length= el 4) (eq? (car el) 'call) (eq? (cadr el) '=>)) + (loop (cdr L) + '() + '() + (merge (merge expr (to-nt current-names current-vals)) + (named-tuple-expr (list (caddr el)) (list (cadddr el)))))) +|# + ((vararg? el) + (loop (cdr L) + '() + '() + (let ((current (merge expr (to-nt current-names current-vals)))) + (if current + (merge current (cadr el)) + `(call (top merge) (call (top NamedTuple)) ,(cadr el)))))) + (else + (error (string "invalid named tuple element \"" (deparse el) "\"")))))))) + (define (expand-forms e) (if (or (atom? e) (memq (car e) '(quote inert top core globalref outerref line module toplevel ssavalue null meta))) e @@ -2246,11 +2316,16 @@ 'tuple (lambda (e) - (if (and (length> e 1) (pair? (cadr e)) (eq? (caadr e) 'parameters)) - (error "unexpected semicolon in tuple")) - (if (any assignment? (cdr e)) - (error "assignment not allowed inside tuple")) - (expand-forms `(call (core tuple) ,@(cdr e)))) + (cond ((and (length> e 1) (pair? (cadr e)) (eq? (caadr e) 'parameters)) + (error "unexpected semicolon in tuple") + ;; this enables `(; ...)` named tuple syntax + #;(if (length= e 2) + (expand-forms (lower-named-tuple (cdr (cadr e)))) + (error "unexpected semicolon in tuple"))) + ((any assignment? (cdr e)) + (expand-forms (lower-named-tuple (cdr e)))) + (else + (expand-forms `(call (core tuple) ,@(cdr e)))))) '=> (lambda (e) @@ -2861,7 +2936,7 @@ f(x) = yt(x) (body (global ,name) (const ,name) ,@(map (lambda (p n) `(= ,p (call (core TypeVar) ',n (core Any)))) P names) (struct_type ,name (call (core svec) ,@P) - (call (core svec) ,@(map (lambda (v) `',v) fields)) + (call (core svec) ,@(map quotify fields)) ,super (call (core svec) ,@types) false ,(length fields)) (return (null)))))))) @@ -2871,7 +2946,7 @@ f(x) = yt(x) (() () 0 ()) (body (global ,name) (const ,name) (struct_type ,name (call (core svec)) - (call (core svec) ,@(map (lambda (v) `',v) fields)) + (call (core svec) ,@(map quotify fields)) ,super (call (core svec) ,@(map (lambda (v) '(core Box)) fields)) false ,(length fields)) @@ -2888,7 +2963,7 @@ f(x) = yt(x) ; (const ,name) ; ,@(map (lambda (p n) `(= ,p (call (core TypeVar) ',n (core Any)))) P names) ; (struct_type ,name (call (core svec) ,@P) -; (call (core svec) ,@(map (lambda (v) `',v) fields)) +; (call (core svec) ,@(map quotify fields)) ; ,super ; (call (core svec) ,@types) false ,(length fields))))) @@ -2897,7 +2972,7 @@ f(x) = yt(x) ; `((global ,name) ; (const ,name) ; (struct_type ,name (call (core svec)) -; (call (core svec) ,@(map (lambda (v) `',v) fields)) +; (call (core svec) ,@(map quotify fields)) ; ,super ; (call (core svec) ,@(map (lambda (v) 'Any) fields)) ; false ,(length fields)))) diff --git a/src/julia.h b/src/julia.h index 32a7d2a62f4b8..4d20c9195f9d2 100644 --- a/src/julia.h +++ b/src/julia.h @@ -374,6 +374,7 @@ typedef struct _jl_datatype_t { struct _jl_datatype_t *super; jl_svec_t *parameters; jl_svec_t *types; + jl_svec_t *names; jl_value_t *instance; // for singletons const jl_datatype_layout_t *layout; int32_t size; // TODO: move to _jl_datatype_layout_t @@ -556,6 +557,8 @@ extern JL_DLLEXPORT jl_datatype_t *jl_voidpointer_type; extern JL_DLLEXPORT jl_unionall_t *jl_pointer_type; extern JL_DLLEXPORT jl_unionall_t *jl_ref_type; extern JL_DLLEXPORT jl_typename_t *jl_pointer_typename; +extern JL_DLLEXPORT jl_typename_t *jl_namedtuple_typename; +extern JL_DLLEXPORT jl_unionall_t *jl_namedtuple_type; extern JL_DLLEXPORT jl_value_t *jl_array_uint8_type; extern JL_DLLEXPORT jl_value_t *jl_array_any_type; @@ -778,7 +781,10 @@ STATIC_INLINE void jl_array_uint8_set(void *a, size_t i, uint8_t x) // struct type info STATIC_INLINE jl_svec_t *jl_field_names(jl_datatype_t *st) { - return st->name->names; + jl_svec_t *names = st->names; + if (!names) + names = st->name->names; + return names; } STATIC_INLINE jl_sym_t *jl_field_name(jl_datatype_t *st, size_t i) { @@ -964,6 +970,12 @@ STATIC_INLINE int jl_is_tuple_type(void *t) ((jl_datatype_t*)(t))->name == jl_tuple_typename); } +STATIC_INLINE int jl_is_namedtuple_type(void *t) +{ + return (jl_is_datatype(t) && + ((jl_datatype_t*)(t))->name == jl_namedtuple_typename); +} + STATIC_INLINE int jl_is_vecelement_type(jl_value_t* t) { return (jl_is_datatype(t) && diff --git a/src/rtutils.c b/src/rtutils.c index e81cc2d2a6035..e3f5957b8cabf 100644 --- a/src/rtutils.c +++ b/src/rtutils.c @@ -862,8 +862,8 @@ static size_t jl_static_show_x_(JL_STREAM *out, jl_value_t *v, jl_datatype_t *vt n += jl_printf(out, ")"); } else if (jl_is_datatype(vt)) { - int istuple = jl_is_tuple_type(vt); - if (!istuple) + int istuple = jl_is_tuple_type(vt), isnamedtuple = jl_is_namedtuple_type(vt); + if (!istuple && !isnamedtuple) n += jl_static_show_x(out, (jl_value_t*)vt, depth); n += jl_printf(out, "("); size_t nb = jl_datatype_size(vt); @@ -896,7 +896,7 @@ static size_t jl_static_show_x_(JL_STREAM *out, jl_value_t *v, jl_datatype_t *vt } n += jl_static_show_x_(out, (jl_value_t*)fld_ptr, ft, depth); } - if (istuple && tlen == 1) + if ((istuple || isnamedtuple) && tlen == 1) n += jl_printf(out, ","); else if (i != tlen - 1) n += jl_printf(out, ", "); diff --git a/src/staticdata.c b/src/staticdata.c index a5e7b75f73f66..0da6a8ac79e61 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -1583,6 +1583,7 @@ static void jl_init_serializer2(int for_serialize) jl_gotonode_type->name, jl_quotenode_type->name, jl_globalref_type->name, jl_typeofbottom_type->name, jl_string_type->name, jl_abstractstring_type->name, + jl_namedtuple_type, jl_namedtuple_typename, jl_int32_type, jl_int64_type, jl_bool_type, jl_uint8_type, @@ -1649,6 +1650,7 @@ static void jl_init_serializer2(int for_serialize) arraylist_push(&builtin_typenames, ((jl_datatype_t*)jl_unwrap_unionall((jl_value_t*)jl_densearray_type))->name); arraylist_push(&builtin_typenames, jl_tuple_typename); arraylist_push(&builtin_typenames, jl_vararg_typename); + arraylist_push(&builtin_typenames, jl_namedtuple_typename); } static void jl_cleanup_serializer2(void) diff --git a/test/ambiguous.jl b/test/ambiguous.jl index 6165813bc395e..6f7f8fc13f7fd 100644 --- a/test/ambiguous.jl +++ b/test/ambiguous.jl @@ -270,6 +270,7 @@ end pop!(need_to_handle_undef_sparam, which(Base._totuple, (Type{Tuple{Vararg{E}}} where E, Any, Any))) pop!(need_to_handle_undef_sparam, which(Base.eltype, Tuple{Type{Tuple{Vararg{E}}} where E})) pop!(need_to_handle_undef_sparam, which(Base.eltype, Tuple{Type{Tuple{Any}}})) + pop!(need_to_handle_undef_sparam, first(methods(Base.same_names))) @test_broken need_to_handle_undef_sparam == Set() pop!(need_to_handle_undef_sparam, which(Base.cat, Tuple{Any, AbstractArray})) pop!(need_to_handle_undef_sparam, which(Base.byteenv, (Union{AbstractArray{Pair{T}, 1}, Tuple{Vararg{Pair{T}}}} where T<:AbstractString,))) diff --git a/test/choosetests.jl b/test/choosetests.jl index 9a749d5bc4e6a..77f1407b9ec11 100644 --- a/test/choosetests.jl +++ b/test/choosetests.jl @@ -37,7 +37,7 @@ function choosetests(choices = []) "bitarray", "copy", "math", "fastmath", "functional", "iterators", "operators", "path", "ccall", "parse", "loading", "bigint", "bigfloat", "sorting", "statistics", "spawn", "backtrace", - "file", "read", "version", "resolve", + "file", "read", "version", "resolve", "namedtuple", "mpfr", "broadcast", "complex", "socket", "floatapprox", "stdlib", "reflection", "regex", "float16", "combinatorics", "sysinfo", "env", "rounding", "ranges", "mod2pi", diff --git a/test/inference.jl b/test/inference.jl index 0530d3012d002..f9dca24b0eb8f 100644 --- a/test/inference.jl +++ b/test/inference.jl @@ -1001,6 +1001,22 @@ copy_dims_pair(out, dim::Colon, tail...) = copy_dims_pair(out => dim, tail...) @test Base.return_types(copy_dims_pair, (Tuple{}, Vararg{Union{Int,Colon}})) == Any[Tuple{}, Tuple{}, Tuple{}] @test all(m -> 5 < count_specializations(m) < 25, methods(copy_dims_pair)) +@test isdefined_tfunc(typeof(NamedTuple()), Const(0)) === Const(false) +@test isdefined_tfunc(typeof(NamedTuple()), Const(1)) === Const(false) +@test isdefined_tfunc(typeof((a=1,b=2)), Const(:a)) === Const(true) +@test isdefined_tfunc(typeof((a=1,b=2)), Const(:b)) === Const(true) +@test isdefined_tfunc(typeof((a=1,b=2)), Const(:c)) === Const(false) +@test isdefined_tfunc(typeof((a=1,b=2)), Const(0)) === Const(false) +@test isdefined_tfunc(typeof((a=1,b=2)), Const(1)) === Const(true) +@test isdefined_tfunc(typeof((a=1,b=2)), Const(2)) === Const(true) +@test isdefined_tfunc(typeof((a=1,b=2)), Const(3)) === Const(false) +@test isdefined_tfunc(NamedTuple, Const(1)) == Bool +@test isdefined_tfunc(NamedTuple, Symbol) == Bool +@test Const(false) ⊑ isdefined_tfunc(NamedTuple{(:x,:y)}, Const(:z)) +@test Const(true) ⊑ isdefined_tfunc(NamedTuple{(:x,:y)}, Const(1)) +@test Const(false) ⊑ isdefined_tfunc(NamedTuple{(:x,:y)}, Const(3)) +@test Const(true) ⊑ isdefined_tfunc(NamedTuple{(:x,:y)}, Const(:y)) + # splatting an ::Any should still allow inference to use types of parameters preceding it f22364(::Int, ::Any...) = 0 f22364(::String, ::Any...) = 0.0 diff --git a/test/namedtuple.jl b/test/namedtuple.jl new file mode 100644 index 0000000000000..42423da7a96b4 --- /dev/null +++ b/test/namedtuple.jl @@ -0,0 +1,191 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +@test_throws TypeError NamedTuple{1,Tuple{}} +@test_throws TypeError NamedTuple{(),1} +@test_throws TypeError NamedTuple{(:a,1),Tuple{Int}} +@test_throws ErrorException NamedTuple{(:a,:b),Tuple{Int}} +@test_throws ErrorException NamedTuple{(:a,:b),Tuple{Int,Vararg{Int}}} +@test_throws ErrorException NamedTuple{(:a,),Union{Tuple{Int},Tuple{String}}} +@test_throws ErrorException NamedTuple{(:a,:a),Tuple{Int,Int}} +@test_throws ErrorException NamedTuple{(:a,:a)}((1,2)) +@test_throws ErrorException NamedTuple{(:a, :b, :a), NTuple{3, Int}}((1, 2, 3)) + +@test fieldcount(NamedTuple{(:a,:b,:c)}) == 3 +@test fieldcount(NamedTuple{<:Any,Tuple{Int,Int}}) == 2 +@test_throws ErrorException fieldcount(NamedTuple) +@test_throws ErrorException fieldcount(NamedTuple{<:Any,<:Tuple{Int,Vararg{Int}}}) + +@test (a=1,).a == 1 +@test (a=2,)[1] == 2 +@test (a=3,)[:a] == 3 +@test (x=4, y=5, z=6).y == 5 +@test (x=4, y=5, z=6).z == 6 +@test_throws ErrorException (x=4, y=5, z=6).a +@test_throws BoundsError (a=2,)[0] +@test_throws BoundsError (a=2,)[2] + +@test length(NamedTuple()) == 0 +@test length((a=1,)) == 1 +@test length((a=1, b=0)) == 2 + +@test (a=1,b=2) === (a=1,b=2) +@test (a=1,b=2) !== (b=1,a=2) + +@test (a=1,b=2) == (a=1,b=2) +@test (a=1,b=2) != (b=1,a=2) +@test NamedTuple() === NamedTuple() +@test NamedTuple() != (a=1,) + +@test string((a=1,)) == "(a = 1,)" +@test string((name="", day=:today)) == "(name = \"\", day = :today)" +@test string(NamedTuple()) == "NamedTuple()" + +@test hash((a = 1, b = "hello")) == hash(NamedTuple{(:a,:b),Tuple{Int,String}}((1, "hello"))) +@test hash((a = 1, b = "hello")) != hash(NamedTuple{(:a,:c),Tuple{Int,String}}((1, "hello"))) +@test hash((a = 1, b = "hello")) != hash(NamedTuple{(:a,:b),Tuple{Int,String}}((1, "helo"))) + +@test NamedTuple{(:a,:b),Tuple{Int8,Int16}}((1,2)) === (a=Int8(1), b=Int16(2)) +@test convert(NamedTuple{(:a,:b),Tuple{Int8,Int16}}, (a=3,b=4)) === (a=Int8(3), b=Int16(4)) +let NT = NamedTuple{(:a,:b),Tuple{Int8,Int16}}, nt = (x=3,y=4) + @test_throws MethodError convert(NT, nt) +end + +@test NamedTuple{(:a,:c)}((b=1,z=2,c=3,aa=4,a=5)) === (a=5, c=3) +@test NamedTuple{(:a,)}(NamedTuple{(:b, :a), Tuple{Int, Union{Int,Void}}}((1, 2))) === + NamedTuple{(:a,), Tuple{Union{Int,Void}}}((2,)) + +@test eltype((a=[1,2], b=[3,4])) === Vector{Int} + +@test Tuple((a=[1,2], b=[3,4])) == ([1,2], [3,4]) +@test Tuple(NamedTuple()) === () +@test Tuple((x=4, y=5, z=6)) == (4,5,6) +@test collect((x=4, y=5, z=6)) == [4,5,6] +@test Tuple((a=1, b=2, c=3)) == (1, 2, 3) + +@test isless((a=1,b=2), (a=1,b=3)) +@test_broken isless((a=1,), (a=1,b=2)) +@test !isless((a=1,b=2), (a=1,b=2)) +@test !isless((a=2,b=1), (a=1,b=2)) +@test_throws MethodError isless((a=1,), (x=2,)) + +@test map(-, (x=1, y=2)) == (x=-1, y=-2) +@test map(+, (x=1, y=2), (x=10, y=20)) == (x=11, y=22) +@test_throws ArgumentError map(+, (x=1, y=2), (y=10, x=20)) +@test map(string, (x=1, y=2)) == (x="1", y="2") +@test map(round, (x=1//3, y=Int), (x=3, y=2//3)) == (x=0.333, y=1) + +@test merge((a=1, b=2), (a=10,)) == (a=10, b=2) +@test merge((a=1, b=2), (a=10, z=20)) == (a=10, b=2, z=20) +@test merge((a=1, b=2), (z=20,)) == (a=1, b=2, z=20) +@test merge(NamedTuple(), (a=2, b=1)) == (a=2, b=1) +@test merge((a=2, b=1), NamedTuple()) == (a=2, b=1) +@test merge(NamedTuple(), NamedTuple()) == NamedTuple() +# `merge` should preserve element types +let nt = merge(NamedTuple{(:a,:b),Tuple{Int32,Union{Int32,Void}}}((1,Int32(2))), + NamedTuple{(:a,:c),Tuple{Union{Int8,Void},Float64}}((nothing,1.0))) + @test typeof(nt) == NamedTuple{(:a,:b,:c),Tuple{Union{Int8,Void},Union{Int32,Void},Float64}} + @test repr(nt) == "NamedTuple{(:a, :b, :c),Tuple{Union{Void, Int8},Union{Void, Int32},Float64}}((nothing, 2, 1.0))" +end + +@test merge(NamedTuple(), [:a=>1, :b=>2, :c=>3, :a=>4, :c=>5]) == (a=4, b=2, c=5) +@test merge((c=0, z=1), [:a=>1, :b=>2, :c=>3, :a=>4, :c=>5]) == (c=5, z=1, a=4, b=2) + +@test keys((a=1, b=2, c=3)) == (:a, :b, :c) +@test keys(NamedTuple()) == () +@test keys((a=1,)) == (:a,) +@test values((a=1, b=2, c=3)) == (1, 2, 3) +@test values(NamedTuple()) == () +@test values((a=1,)) == (1,) +@test haskey((a=1, b=2, c=3), :a) +@test !haskey(NamedTuple(), :a) +@test !haskey((a=1,), :b) +@test get((a=1, b=2, c=3), :a, 0) == 1 +@test get(NamedTuple(), :a, 0) == 0 +@test get((a=1,), :b, 0) == 0 +@test get(()->0, (a=1, b=2, c=3), :a) == 1 +@test get(()->0, NamedTuple(), :a) == 0 +@test get(()->0, (a=1,), :b) == 0 + +# syntax errors + +@test Meta.lower(Main, parse("(a=1, 0)")) == Expr(:error, "invalid named tuple element \"0\"") +@test Meta.lower(Main, parse("(a=1, f(x))")) == Expr(:error, "invalid named tuple element \"f(x)\"") +@test Meta.lower(Main, parse("(a=1,a=2)")) == Expr(:error, "field name \"a\" repeated in named tuple") +@test Meta.lower(Main, parse("(a=1,b=0,a=2)")) == Expr(:error, "field name \"a\" repeated in named tuple") +@test Meta.lower(Main, parse("(c=1,a=1,b=0,a=2)")) == Expr(:error, "field name \"a\" repeated in named tuple") + +@test parse("(;)") == quote end +@test Meta.lower(Main, parse("(1,;2)")) == Expr(:error, "unexpected semicolon in tuple") + +# splatting + +let d = [:a=>1, :b=>2, :c=>3] # use an array to preserve order + @test (d..., a=10) == (a=10, b=2, c=3) + @test (a=0, b=0, z=1, d..., x=4, y=5) == (a=1, b=2, z=1, c=3, x=4, y=5) + @test (a=0, (b=2,a=1)..., c=3) == (a=1, b=2, c=3) +end + +# inference tests + +namedtuple_get_a(x) = x.a +@test Base.return_types(namedtuple_get_a, (NamedTuple,)) == Any[Any] +@test Base.return_types(namedtuple_get_a, (typeof((b=1,a="")),)) == Any[String] + +namedtuple_fieldtype_a(x) = fieldtype(typeof(x), :a) +@test Base.return_types(namedtuple_fieldtype_a, (NamedTuple,)) == Any[Type] +@test Base.return_types(namedtuple_fieldtype_a, (typeof((b=1,a="")),)) == Any[Type{String}] +namedtuple_fieldtype__(x, y) = fieldtype(typeof(x), y) +@test Base.return_types(namedtuple_fieldtype__, (typeof((b=1,a="")),Symbol))[1] >: Union{Type{Int}, Type{String}} + +namedtuple_nfields(x) = nfields(x) === 0 ? 1 : "" +@test Union{Int,String} <: Base.return_types(namedtuple_nfields, (NamedTuple,))[1] + +function nt_from_abstractly_typed_array() + a = NamedTuple[(a=3,b=5)] + (getfield(a[1],1), getfield(a[1],2)) +end +@test nt_from_abstractly_typed_array() === (3,5) + +let T = NamedTuple{(:a, :b), Tuple{Int64, Union{Float64, Void}}}, nt = T((1, nothing)) + @test nt == (a=1, b=nothing) + @test typeof(nt) == T + @test convert(T, (a=1, b=nothing)) == nt + @test typeof(convert(T, (a=1, b=nothing))) === T +end + +function abstr_nt_22194() + a = NamedTuple[(a=1,), (b=2,)] + return (a[1].a, a[2].b) +end +@test abstr_nt_22194() == (1, 2) +@test Base.return_types(abstr_nt_22194, ()) == Any[Tuple{Any,Any}] +function abstr_nt_22194_2() + a = NamedTuple[(a=1,), (b=2,)] + return a[1].b +end +@test_throws ErrorException abstr_nt_22194_2() +@test Base.return_types(abstr_nt_22194_2, ()) == Any[Any] + +mutable struct HasAbstractNamedTuples + x::NamedTuple{(:a,:b)} +end + +function abstr_nt_22194_3() + s = HasAbstractNamedTuples((a="",b=8)) + @test s.x.a === "" + @test s.x.b === 8 + s.x = (a=1,b=:b) + @test s.x.a === 1 + @test s.x.b === :b + @test isdefined(s.x, :a) + @test isdefined(s.x, :b) + @test !isdefined(s.x, :c) + @test nfields(s) == 1 + @test isdefined(s, :x) + @test fieldtype(typeof(s), 1) == fieldtype(typeof(s), :x) == NamedTuple{(:a,:b)} + @test fieldtype(typeof(s.x), :a) === Int + @test fieldtype(typeof(s.x), :b) === Symbol + return s.x.b +end +abstr_nt_22194_3() +@test Base.return_types(abstr_nt_22194_3, ()) == Any[Any]