Repository navigation
Category Tuples
Home › All Categories › Tuples
declare function MinMax( a( ) as int32 ) as (min as int32, max as int32)
var (lo, hi) = MinMax( arr( ) )
dim p as (int32, ustring) = (3, "hi")
dim q as (int32, ustring) = (3, "hi")
print p = q ' trueA tuple is an anonymous aggregate spelled '(T, U, ...)' in any type position and '(a, b, ...)' in any expression position. No '#include', no namespace, no runtime support -- it is a language feature.
Without tuples, a function that returns two values has to pick one of three workarounds, and all three are worse than the problem:
declare sub MinMax1( a( ) as int32, byref lo as int32, byref hi as int32 )
type MinMaxResult ' a TYPE used in exactly one place
as int32 lo, hi
end type
declare function MinMax2( a( ) as int32 ) as MinMaxResult
dim shared as int32 g_lo, g_hi ' the honest version of the same thing
declare sub MinMax3( a( ) as int32 )An out-parameter cannot be used in an expression and cannot be const. A one-off TYPE has to be named, declared somewhere, and then read by whoever calls the function. A shared global is not re-entrant.
dim p as (int32, ustring)
dim q as (float32, float32, float32)
dim n as ((int32, int32), ustring) ' elements nest
type Pair as (int32, ustring) ' a typedef, if you want a name
declare function f( ) as (int32, int32)
declare sub g( byval p as (int32, int32) )
declare sub h( byref p as (int32, int32) )Minimum arity is TWO. '(long)' is a syntax error, not a 1-tuple -- see NO 1-TUPLES below.
There is no maximum. A tuple has as many elements as it is written with, and the return type, the literal and destructuring work the same at three elements as at two:
function foo( ) as (a as int32, b as int64, c as float64)
return (10, 20, 30.45)
end function
var (d, e, f) = foo( )
print d, e, f ' prints: 10 20 30.45The fields are named '_1' through '_N':
p._1 = 3
p._2 = "hi"
print p._1, p._2Layout is the layout of the equivalent hand-written TYPE -- same size, same offsets, same alignment -- because a tuple IS an ordinary UDT underneath. A STRING element is constructed, copied and destroyed exactly as a STRING field is.
SIZEOF, LEN and TYPEOF take a tuple type written inline, or one named by a typedef:
type P as (int32, string)
print sizeof( (int32, string) ), sizeof( P ) ' 32 32 on windows-x86_64An element may carry a name. The name is checked for duplicates within the declaration.
The name also reaches its element:
function GetRect( ) as (tl as (x as int32, y as int32), br as (x as int32, y as int32))
return ((100, 200), (300, 400))
end function
var r = GetRect( )
print r.tl.x, r.tl.y ' 100 200
print r._1._1, r._1._2 ' the same elementsCode can also use _1.._N, or destructure:
var (lo, hi) = MinMax( arr( ) )THE NAMES ARE NOT PART OF THE TYPE. (min as int32, max as int32) and (int32, int32) are still one type -- see below -- with one layout and one mangled name. The names travel with the DECLARATION that an expression reaches, which is C#'s rule:
type Pt as (x as int32, y as int32)
dim a as Pt = (1, 2)
dim b as (lo as int32, hi as int32) = a ' one type, so assignable
print b.lo, a.x ' each read with its own declaration's names- a variable, parameter or field takes the names in its own type, through a typedef or a pointer (
p->x); -
var r = f( )takes the names of f's result; - a call's result is named by the function's declaration:
GetRect( ).br.y; - an array element takes the array's element names:
arr( 1 ).x; - a WITH block's
.nameuses the names of the WITH subject.
A POSITIONAL NAME ALWAYS WINS. Over (_2 as int32, _1 as int32), t._1 is the FIRST element.
What carries no names, so only _1.._N reach its elements:
- a tuple LITERAL --
(lo, hi)does not name its elements after the variables; - a
for eachloop variable, an IIF or cast result, and a tuple written inside a generic template.
An unknown name is TKO0739.
Two tuple types are THE SAME TYPE when they have the same element types in the same order. How they are spelled, and what the elements are called, never enters:
dim a as (int32, int32)
dim b as (min as int32, max as int32)
dim c as (lo as int32, hi as int32)
type Pair as (int32, int32) : dim d as Pair
type MyLong as int32 : dim e as (MyLong, MyLong)All five are one type, mutually assignable, and one interned symbol program-wide. This is C#'s rule, not Nim's -- Nim makes field names part of tuple equivalence.
THE CONSEQUENCE, STATED PLAINLY: a tuple cannot give you nominal type safety. If '(long, long)' meaning a screen coordinate must be rejected where '(long, long)' meaning min/max is expected, a tuple is the wrong tool and a TYPE is the right one. Tuples are for values grouped by position at a call boundary, not for domain types.
A parenthesised comma list of two or more expressions:
dim t as (int32, ustring) = (3, "hi")
t = (4, "bye")
return (lo, hi)
Consume( (1, 2) ) ' ONE argument, of type (long, long)Elements are evaluated left to right, exactly once each.
dim t as (float64, ustring) = (1, "hi") ' 1 -> 1.0, "hi" -> USTRING
dim u as (int32, int32) = (cshort(1), cbyte(2))
function f( ) as (float64, ustring)
return (0, "") ' converts per element
end function...but two tuple VALUES of different types do not convert:
dim a as (int32, int32)
dim b as (float64, float64) = a ' error: type mismatchThe literal is not converted -- it is BUILT AT the target type, each element converted as it is stored into its field. That works in the four places the target type is known: assignment, DIM initialisation, RETURN, and a call argument.
Arity is never inferred and never padded, in any direction:
dim v as (int32, int32) = (1, 2, 3) ' error TKO0618: too many expressionsA NESTED literal is context-typed too, each inner literal at its element's type:
dim ok as ((float64, float64), int32) = ((1, 2), 3) ' fine
dim n as ((int32, string), int32) = ((1, "x"), 3) ' fineTwo forms. 'var' declares; the bare paren list assigns.
var (lo, hi) = MinMax( arr( ) ) ' declares lo and hi, infers both
dim x as int32, y as ustring
(x, y) = GetPair( ) ' assigns to existing lvalues'var (…)' takes plain identifiers only -- no AS clause, no subscripts, no SHARED, no STATIC. It is a declaration, not an lvalue list. Scoping and shadowing follow the ordinary VAR rules.
The assigning form takes ANY lvalue:
(h.tag, h.label) = (7, "seven")
(arr(2), arr(3)) = (8, 9)
(*p, arr(2)) = (10, 11)THE RIGHT-HAND SIDE IS EVALUATED ONCE, and bound before any target is written, which is what makes this a swap rather than a copy:
(a, b) = (b, a) ' a and b are swappedTargets are assigned left to right. Overlap is NOT diagnosed: '(a, a) = f( )' assigns '_1' then '_2' to the same variable and leaves '_2'. Detecting that in the presence of 'arr(i)' and '*p' targets is not decidable.
Arity must match EXACTLY in both directions: too many or too few targets is TKO0825, which says how many elements the tuple has and how many targets are written.
The right-hand side must be a tuple. A plain UDT is TKO0826:
(n1, n2) = someUdt ' error TKO0826: a tuple is expected hereLET TAKES ANY UDT. 'LET(a, b) = anyUdt' walks any UDT's fields in declaration order, checks only the too-many direction, and accepts a tuple, since a tuple is a UDT. Positional unpacking of a NAMED type is LET-only on purpose: reordering that type's fields silently changes what every unpack site means, and that is not a failure mode worth spreading. tiko implements only #lang "fb", where both forms work.
A collection whose iterator's Value( ) returns a tuple can be walked with the element destructured in place:
for each (k, v) in m.Pairs( )
print k, v
nextThe element is bound once and each name is declared from one field of it, so this is the ordinary FOR EACH lowering with a destructure on top. Both names are copies.
Neither AS nor BYREF is accepted on the list form. One type cannot describe elements that differ, per-name types would spell out what the tuple already says, and binding the whole element by reference is what 'for each byref e' already does. Use that and reach through '_1' / '_2' if you need a reference.
The element must be a tuple and the name count must match its arity; both are diagnosed.
'=' and '<>' work element-wise:
t = u is t._1 = u._1 andalso t._2 = u._2 andalso …
t <> u is t._1 <> u._1 orelse t._2 <> u._2 orelse …'<>' is deliberately NOT built as 'not (t = u)'. With a NaN element that would be wrong; building both chains directly gives IEEE behaviour per element for free.
THE COMPARISON SHORT-CIRCUITS, THE OPERANDS DO NOT. '(F(1), F(2)) = (F(3), F(4))' makes all four calls -- they are the elements of two literals, and a literal evaluates every element exactly once -- and only then compares, stopping at the first mismatch.
Both sides must be the SAME tuple type, which after structural interning is a pointer comparison. Different arity, or the same arity with different element types, is a type mismatch: a comparison supplies no target type, so nothing context-types anything.
dim x1 as (int32, int32)
dim x2 as (float64, float64)
print (x1 = x2) ' error TKO0720: no operator = compares this typeEvery element type must support '='. A UDT element without one fails with that UDT's own diagnostic, at the element.
A user-written global 'operator =' over a tuple type WINS over the built-in chain -- the built-in is tried after ordinary overload resolution, not before.
'<', '<=', '>' and '>=' are NOT defined. Lexicographic order is only sometimes what the writer meant, and when it is not, the code still compiles and silently sorts wrong; equality has no such ambiguity. It is a pure addition if it is ever wanted. Write the comparison you mean:
if a < b orelse (a = b andalso c < d) then'(x)' is a grouped expression, permanently. There is no '(x,)'.
This is not an oversight. '(a) = 5' is legal, and 'Consume( (a) )' is the idiom for forcing a byval copy at a byref parameter. A 1-tuple spelling would put a second meaning on '(x)' and add a trailing-comma escape to a language that does not need one.
Arity 0 is not spellable either -- '()' has no use without a unit type.
const c as (int32, int32) = 0 ' error TKO0748
type Derived extends (int32, int32) ' error TKO0824EXTENDS is excluded because a tuple has no vtable, no RTTI and no identity to inherit; CONST is excluded because there is nothing a tuple constant could be initialised from. tiko reports TKO0748 for a tuple constant and TKO0824 for EXTENDS. 'expr IS T' is excluded too: a tuple has no run-time type information, and IS reports TKO0799.
PRINT of a whole tuple is a deliberate error, TKO0871:
print t ' error TKO0871: no printable form
print t._1, t._2 ' say what you meanAn element-wise PRINT was considered and rejected -- 'print (a, b)' would then be indistinguishable at a glance from 'print a, b', and the ';' and USING forms would each need a rule. WRITE, LPRINT and the file-I/O quirks behave identically.
Accepted: every scalar, STRING, USTRING, ANY PTR, ZSTRING PTR, function pointers, forward-referenced POINTERS, UDTs with constructors and destructors, and nested tuples.
Rejected:
ANY TKO0823: has no size; nothing could be stored
STRING * n TKO0823: the '* n' suffix has no positional spelling
a dynamic array TKO0401: a field cannot be one without a descriptor pairing
a non-pointer forward reference TKO0810: the type must be complete when internedOnly the FIRST bad element in a list is reported; the rest are suppressed. Parsing continues past a bad element so the list is consumed, not so that every element gets its own message.
There is no upper bound on arity.
'(' has three other meanings -- a grouped expression, a call argument list, an array subscript list -- and a comma is legal inside one of them. Every one of these compiles with that meaning:
(a) = 5 ' parenthesised lvalue
(*p) = 5
(arr(1,1)) = 5
(x).f = 1
s1( (a) ) ' the byval-override idiom
s2 (1, 2) ' paren-less call: TWO arguments
(pfn)( b ) ' function-pointer call
print (a)
print sizeof( (a) )
print arr(1, 2)
pset (10, 20)
line (0, 0)-(10, 10)
circle (10, 10), 5Call argument lists and subscript lists never reach the expression parser's paren handler at all, and the graphics quirks match their own '('. That leaves the grouped expression and the paren-less call, and both go through one function which parses the FIRST element before it has to decide anything -- so the fourth meaning is separated with NO lookahead and NO backtracking. A comma is a tuple only when the parens could not belong to a paren-less call, and only when the surrounding context is not demanding some other type.
A comma inside a grouped expression gives these forms their meaning:
(a, b) = (1, 2) destructuring
Consume( (1, 2) ) one tuple argument
if (a, b) = (c, d) element-wise comparison
print (a, b) TKO0871: a tuple has no printable formTwo modules that both mention '(long, string)' must produce the same external symbol, or a tuple-returning function cannot be called across a module boundary. Nothing is shared between them and nothing is written to disk: each module RE-DERIVES the name from the element types alone.
That is why the name is a pure function of the element types and of nothing else -- not a counter, not declaration order, not the compilation order, and not element names:
(int32, int32) -> 8FBTUPLE2IiiE
(int32, string) -> 8FBTUPLE2Ii8FBSTRINGE
((int32,int32), string) -> 8FBTUPLE2I8FBTUPLE2IiiE8FBSTRINGESo the two sides may spell the element names differently, or not at all, and still link.
tiko reports:
- an unterminated element list, and
var (arr(1), b) = ...(TKO0401: expected ')'); - a tuple constant (
TKO0748: a constant cannot have the type of 'c'); - a 1-tuple (
TKO0821), a duplicated element name (TKO0822), and an ANY or fixed-length-string element (TKO0823) -- only the first problem in a list reported; -
extends (int32, int32)(TKO0824; a typedef of a tuple afterextendscompiles); - destructuring with too few or too many targets, and
var (a, b)of the wrong arity (TKO0825), over a UDT or a scalar (TKO0826), andvar (a) = 1(TKO0821).let ( a, b ) = udtis the LET multiple assignment, which takes a UDT's fields in order, and is accepted; -
<,>,<=and>=on tuples, and=or<>between two DIFFERENT tuple types --(int32, int32)against(float64, float64), against the literal(1, 2), which is(integer, integer), or against a scalar (TKO0720). One tuple type on both sides compares.
print of a tuple is TKO0871, an initialiser with more elements than the tuple is TKO0618, and expr is a tuple type is TKO0799.
A tuple element whose value is of ANOTHER type is converted by the element type's own operator let, over an element that has already been constructed -- return ( wszKLID, bRes ) out of a function as (DVARIANT, BOOLEAN) takes DVARIANT's let from a wstring. Without a let that takes the value, tiko refuses the element with TKO0831. A let from a int32 takes a float64 and not a string; a constructor from the value's type is not enough. A plain type -- no constructor, no string field, no extends object -- takes a number without one. A string element does not take a number, nor a number element a string.
A tuple VALUE of another tuple type is refused everywhere, since two tuple types never convert. A (int32, int32) target given a (integer, integer) variable gets TKO0738 in an initialiser, an assignment, a return or function =. As an argument, byval or byref, it gets TKO0698. As an element of a literal, as in return (t, 3), it gets TKO0831. The fix is to declare the value at the target's type, dim as (int32, int32) t = (5, 6), or to write the literal where it is used.
A comparison of two tuples of one type is an integer, -1 or 0: print a = b, str( a = b ) and -( a = b ) all work. A tuple compares only if every element does. A UDT element needs the operator written for that comparison, over two of itself: with only operator = declared for it, = works and <> is TKO0720, and an operator cast does not help. A nested tuple element is judged by its own elements.
tiko's implementation is in its type table and its MIR lowering.
tiko mangles a tuple type as SEPARATE COMPILATION above says: an instantiation of 'FBTUPLE', its element types the arguments. Two separately compiled tiko modules sharing a namespaced procedure over '(Pt, long)', with Pt declared in a different order in each, link and run.
The tuple/ tests, vendored in tiko-lang at tests/vendor/fbc-tests/tuple/:
basic.bas every accepted element kind; '_1'.._N read and write; a
tuple as a field, an array element, a byval and a byref
parameter, and a result. SIZE AND FIELD OFFSETS ARE
ASSERTED AGAINST THE EQUIVALENT HAND-WRITTEN TYPE rather
than against hardcoded numbers, which would only test win64.identity.bas named, differently-named, unnamed, typedef'd and
alias-elemented spellings are one type. Every assignment IS
the assertion -- distinct types would not compile.literal.bas construction in all four context-typed positions;
left-to-right single evaluation with call counts asserted;
nesting; and DESTRUCTION BALANCE with a counting element
type, which is the half that is invisible with scalar data.dtor.bas ctor/dtor counts for a tuple of STRING and of a UDT with a
destructor, including temporaries.destructure.bas both forms against all three shapes of right-hand side --
variable, literal, call; field / array / deref targets;
the swap; a call evaluated exactly once; per-element
conversion; a STRING element surviving as an independent
copy; a loop with no accumulation; and the LET form.equality.bas '=' and '<>' over scalar, STRING, pointer and nested
elements; equality across two differently-named
declarations; a NaN element -- 't = u' false AND 't <> u'
true, which is what breaks if '<>' is built as 'not (=)';
short-circuit proven with a counting element type.equality-overload a user-declared global 'operator =' wins. for-each.bas 'for each (k, v)' over a collection whose Value( ) returns
a tuple with a STRING element -- the shape a real map has,
and the shape that found the TYPE-body crash above.no-regress.bas COMPILE_ONLY: every line of the "other meanings"
list above. This file gates the literal.tuple-mm-*.bas MULTI-MODULE: the mangling test. One module defines the
tuple-returning functions, the other declares them WITH
DIFFERENT element names and calls them. Only a
link-compatible name passes.tests/vendor/fbc-tests/errors/tuple-errors.bas collects the error cases.
errors/tuple-foreach-errors.bas, beside it, covers the 'for each (k, v)' diagnostics. AS and BYREF on the list form get a file each: they are rejected while the binding is still being read, before the FOR compound is pushed, so the error skip leaves the following 'next' orphaned and suppresses every later diagnostic in the same file.
Under tiko: basic, identity, literal, dtor, destructure, equality, equality-overload and for-each build and run with exit 0; no-regress.bas compiles; and the two-module tuple-mm-main.bas + tuple-mm-a.bas builds and runs clean when both files are given to one tiko invocation (see IMPLEMENTATION for separate compilation). tiko's own suite: tests/types/tuple-type.test and eight tests/mir/tuple-*.test files.
tiko-lang · Home · Feature Index · Keyword Index · License
FreeBASIC manual pages reproduced under the GNU FDL 1.3.
- Home
- What Is tiko-lang
- Quick Start
- Installing on macOS
- Debugging
- Feature Index
- Compatibility
- FAQ
- AI Cheatsheet
- Generics
- Interfaces
- For Each
- Iterators
- Tuples
- Array Results
- Defer
- Lambdas
- Block Strings
- Partial Types
- String Slices
- Embed
- Typed Variadics
- C Header Import
- Foreign Library Linking
- UString
- Fixed-Width Types
- Nullptr
- String Library
- Format
- Array
- Map
- Set
- LinkedList
- OrderedMap
- OrderedSet
- Sorting
- Optional
- Result
- Smart Pointers
- Date and Time (Chrono)
- Time Zones
- Filesystem
- File Streams
- Config (INI and TOML)
- Codec
- Hash
- UUID
- OS
- Process
- Net
- HTTP and TLS
- Math Library
- Finance
- Logging
- Terminal TUI Library
- PsControls
- tkControls
- sys/win
- Goroutines and Channels
- LLVM Backend
- tiko build
- TIKO_COMPILER
- Variable Declarations
- Standard Data Types
- User Defined Types
- Procedures
- Operators
- Control Flow Statements
- Converting Data Types
- Array Functions
- String Functions
- Mathematical Functions
- Bit Manipulation
- Date and Time Functions
- File I/O Functions
- Console Functions
- User Input (Runtime Library)
- User Input Functions (Graphics Library)
- Screen Functions
- 2D Drawing Functions
- Graphics Library Reference
- Graphics Keyword List
- Memory Functions
- Threading Support Functions
- Error Handling Functions
- Operating System Functions
- Preprocessor
- Intrinsic Defines
- Miscellaneous Keywords
- Modularizing