aboutsummaryrefslogtreecommitdiffhomepage
path: root/notes/lifetimes.txt
blob: 120d66e8d2e14d1a79820627b50ca5adf7c1db44 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164

Lifetimes
=========

Normal, no lifetime change:

 func print(ref T p) -> void

Taking ownership:

 func destroy(own T p) -> void

Giving ownership:

 func create() -> own T

Letting parameter borrow:

 func add(ref T container, ref T content_to_add) -> void
     lifetime content_to_add >= container

Letting returned value borrow
(note: no operation on "container" may reduce the lifetime
of the returned item after a call like this)

 func get(ref T container) -> ret T
     lifetime return >= container

Giving ownership with return-related lifetime constraint on parameter:

 func create(ref T p) -> own T
    lifetime p >= return



Rules for lifetime specifiers:
- the left side must NOT be "own" (how about "arena"?)
- if the right side is a parameter, then the parameter must be "ref" (XXX check this)
- if the right side is "return", then the return must be "own"
- a returned value MUST either be "own", or it must be on the left side of a lifetime specifier, with the return on the right side.

lifetimes and aliasing
----------------------
An object that references another object, could alias other objects.
For this reason, it is meaningful to allow qualifiers on lifetime items:

- "lifetime p >= q" p may be read by operations on q.
- "lifetime var p >= q" p may be read and modified by (var) operations on q.
- "lifetime addr p >= q" p may not be accessed by operations on q.
- "lifetime writeonly p >= q" p may only be written to by operations on q.
- "lifetime readonly p >= q" p may be read be operations on q (useful when p is "var", for example)

The default should be inferred from the qualifier of the parameter.

Threaded access could be allowed by specifying "threaded" on the parameter.


Various notes
-------------
    /* FIXME should we have two sets of lifetimes?
             - lifetimes at start of function call (in)
             - lifetimes at end of function call (out)
             (and lifetimes that apply to both start and end of call) */
    /* FIXME Also, lifetime can be transferred.
             - e.g. from a parameter to a data structure
             - so in/out keywords can appear on both sides of the >=
             XXX maybe this can be inferred practially at all times?
                 - own parameter = in
                 - own return = out
                 => so "own" without lifetime = unbounded lifetime transfer
                    and "own" with lifetime = bounded lifetime transfer
                    and "ref" with lifetime = bounded lifetime borrow
                    and "ref" without lifetime = implicit (input: borrow during call, return: lifetime == inherit arena lifetime)
             Do we need == also?
             - "lifetime in param1 == out obj.param2
             What should happen with the "unspecified side"
             (e.g. "out" when "in" is specified)?
             - By default, parameters are owned by the caller
             */
    /* FIXME Complicated lifetimes can be constructed with type
             qualifiers: "ref own" (caller most likely still have the
             ref somewhere, it has to be prevented from using it) */
    /* TODO it would be nice to have "virtual variables" tied to
            private types that can have lifetimes to (and also to
            be able to track fields of non-private structs also)
            - related: pointers-to-pointers and lifetimes */
    /* TODO future expansion: lifetimes tied to type states
            (e.g. gives lifetime only in a certain typestate) */

* How to prevent leaks with cycling own-references?
    type Thing = {
        own Thing th
    }
    func f(arena) {
        own Thing th = .allocate(arena)
        th.th = th  # <--- !!!
    }

* Lifetimes of static data?
    - It would be nice to support this!
    - This is NOT the same thing as "infinite" lifetime,
      if the system allows modules to be unloaded (i.e. any system with
      dynamic loading, e.g. dlopen/dlclose, LoadLibrary/FreeLibrary, etc)

    # returns an item in the static const area of this module
    func f() -> static Item

    # returns an item in the static const area of the module "othermod"
    # (should it be required to be in \interface_depends, or only \depends ?)
    func f() -> ref Item
        lifetime return >= static(othermod)

    - What should the keyword be?
        - modown   <-- most logical in return values? but could be
                       confused with ownership of the module itself!
        - modref   <-- most logical in parameters?
        - static   <-- familiar for C, C++ and Java programmers?
                       but could also be confusing if used in structs.

    How to declare that a function unloads a module?
    - Perhaps a special "module" type?

        func .load(string name) -> own module
        func .reload(own module mod) -> own module
        func own module.unload()

* Qualifiers for common cases? (maybe not needed)
    - Object-owned data:
        func ref SomeThing.get_details() -> thisown Details
        - What should the keyword be?
            - thisown
            - objown
            - ref(this)
            - own(this)
            - thisref
        - BUT a plain "ref Thing" return does NOT make any sense without
          a lifetime. So the default lifetime could be:
            1. that of the "this" ref
            2. that of the module

* Returning a struct (on stack) with references to owned objects?
    - Would need some special syntax...
    - perhaps a "stackown" qualifier?

* Add some way of safely calling non-SLUL code, without triggering
  undefined behavior. SLUL assumes the following:
    - a function can ONLY access it's parameters (and nested objects),
      and only with the access specified by the type qualifier /
      reference type.
    - all I/O, including memory allocation, must be done through an arena,
      which must be reachable via the called function's parameters
      (taking qualifiers into account, so at least one parameter must
      be marked "arena").

    - Add a keyword?
        - system
        - native      <-- this is what Java uses.
        - extern      <-- this is what Rust uses.

    # Which syntax to use?
    func do_stuff() -> int system
    sysfunc do_stuff() -> int
    syst func do_stuff() -> int
    system func do_stuff() -> int
    func system do_stuff() -> int   # <-- best choice?