diff --git a/.gitignore b/.gitignore index d3d42d9f3..3bd7b8ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /build +/cmake-build-debug /docs/_site /docs/_vendor /install @@ -18,6 +19,13 @@ /tests/dynlib /tests/dynlib.exe /tests/not_bc +/tests/class2 +/tests/inline_c.exe +/tests/objc +/tests/objc2 +/tests/stdio.exe + +/.idea *.bc *.ll diff --git a/lib/raii.md b/lib/raii.md new file mode 100644 index 000000000..5a7771511 --- /dev/null +++ b/lib/raii.md @@ -0,0 +1,152 @@ +# RAII - Resource management +Resource acquisition is initialization ([RAII](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization)) provides a deterministic means of safe resource management. It is generally associated with systems programming languages such as *c++* and *rust*. + +In the following I summarize the experimental implementation that you can find [here](https://github.com/renehiemstra/terra/tree/raii). The socalled [Big Three](https://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming)) are supported: +* object destruction +* copy assignment +* copy construction + +Terra does not support rvalue references (introduced e.g. in *c++11*), so the experimental RAII support is comparable to that of *C++03* or *rust*. + +## Feature summary +Compiler support for the following methods: +``` +A.methods.__init(self : &A) +A.methods.__dtor(self : &A) +(A or B).methods.__copy(from : &A, to : &B) +``` +These methods support the implementation of smart containers and smart pointers, like *std::string*, *std::vector* and *std::unique_ptr*, *std::shared_ptr*, *boost:offset_ptr* in C++. + +The design does not introduce any breaking changes. No new keywords are introduced. Heap resources are acquired and released using the regular *C stdlib* functions such as malloc and free, leaving memory allocation in the hands of the programmer. + +If implemented, these methods are inserted judiciously during the type checking phase, implemented in *terralib.lua*. All these metamethods can be implemented as macro's or as terra functions. + +## Compiler supported methods for RAII +A managed type is one that implements at least `__dtor` and optionally `__init` and `__copy` or, by induction, has fields or subfields that are of a managed type. In the following I assume `struct A` is a managed type. + +To enable RAII, import the library */lib/terralibext.t* using +``` + require "terralibext" +``` +The compiler only checks for `__init`, `__dtor` and `__copy` in case this libreary is loaded. + +### Object initialization +`__init` is used to initialize managed variables: +``` + A.methods.__init(self : &A) +``` +The compiler checks for an `__init` method in any variable definition statement, without explicit initializer, and emits the call right after the variable definition, e.g. +``` + var a : A + a:__init() --generated by compiler +``` +### Copy assignment +`__copy` enables specialized copy-assignment and, combined with `__init`, copy construction. `__copy` takes two arguments, which can be different, as long as one of them is a managed type, e.g. +``` + A.metamethods.__copy(from : &A, to : &B) +``` +and / or +``` + A.metamethods.__copy(from : &B, to : &A) +``` +If `a : A` is a managed type, then the compiler will replace a regular assignment by a call to the implemented `__copy` method +``` + b = a ----> A.methods.__copy(a, b) +``` +or +``` + a = b ----> A.methods.__copy(b, a) +``` +`__copy` can be a (overloaded) terra function or a macro. + +The programmer is responsable for managing any heap resources associated with the arguments of the `__copy` method. + +### Copy construction +In object construction, `__copy` is combined with `__init` to perform copy construction. For example, +``` + var b : B = a +``` +is replaced by the following statements +``` + var b : B + b:__init() --generated by compiler if `__init` is implemented + A.methods.__copy(a, b) --generated by compiler +``` +If the right `__copy` method is not implemented but a user defined `__cast` metamethod exists that can cast one of the arguments to the correct type, then the cast is performed and then the relevant copy method is applied. + +### Object destruction +`__dtor` can be used to free heap memory +``` + A.methods.__dtor(self : &A) +``` +The implementation adds a deferred call to `__dtor ` near the end of a scope, right before a potential return statement, for all variables local to the current scope that are not returned. Hence, `__dtor` is tied to the lifetime of the object. For example, for a block of code the compiler would generate +``` +do + var x : A, y : A + ... + ... + defer x:__dtor() --generated by compiler + defer y:__dtor() --generated by compiler +end +``` +or in case of a terra function +``` +terra foo(x : A) + var y : A, z : A + ... + ... + defer z:__dtor() --generated by compiler + return y +end +``` +`__dtor` is also called before any regular assignment (if a __copy method is not implemented) to free 'old' resources. So +``` + a = b +``` +is replaced by +``` + a:__dtor() --generated by compiler + a = b +``` +## Compositional API's +If a struct has fields or subfields that are managed types, but do not implement `__init`, `__copy` or `__dtor`, then the compiler will generate default methods that inductively call existing `__init`, `__copy` or `__dtor` methods for its fields and subfields. This enables compositional API's like `vector(vector(int))` or `vector(string)`. This is implemented as an extension to *terralib.lua* in *lib/terralibext.t*. + +## Examples +The following files have been added to the terra testsuite: +* *raii.t* tests whether `__dtor`, `__init`, and `__copy` are evaluated correctly for simple datatypes. +* *raii-copyctr-cast.t* tests the combination of `metamethods.__cast` and `__copy`. +* *raii-unique_ptr.t* tests some functionality of a unique pointer type. +* *raii-shared_ptr.t* tests some functionality of a shared pointer type. +* *raii-offset_ptr.t* tests some functionality of an offset pointer implementation, found e.g. in *boost.cpp*. +* *raii-compose.t* tests the compositional aspect. + +You can have a look there for some common code patterns. + +## Current limitations +* The implementation is not aware of when an actual heap allocation is made and therefore assumes that a managed variable always carries a heap resource. It is up to the programmer to properly initialize pointer variables to nil to avoid calling 'free' on uninitialized pointers. +* Tuple (copy) assignment (regular or using `__copy`) are prohibited by the compiler in case of managed variables. This is done to prevent memory leaks or unwanted deletions in assignments such as +``` + a, b = b, a +``` +* Currently, there is no way to prevent unwanted calls to `__dtor` in cases such as the following. Consider +``` +terra foo() + var b : A + return bar(b) +end +``` +which will get expanded to +``` +terra foo() + var b : A + defer b:__dtor() --generated by compiler + return bar(b) +end +``` +If `bar` would return `b` then its associated heap resources would be released before they can be used in the outer scope. + +## Roadmap +The current implementation already works in a range of important applications, as can be seen in the tests and the examples above. To remove the noted limitations and to enable graceful compile-time errors my plan is to: +* support *affine types* (similar to *rust*) by checking, at compile time, that a managed variable is used (passed by value) not more than once. This essentially means that the variable is moved from (not copied) on every use. This is not restrictive, since in general you would pass managed objects by reference, not by value. +* support borrow checking (similar to *rust*) by counting, at compile time, the number of references. +* introduce a `__new` method that signals a heap allocation. This way the compiler is made aware of all heap allocations being made. \ No newline at end of file diff --git a/lib/terralibext.t b/lib/terralibext.t new file mode 100644 index 000000000..8777e0a0d --- /dev/null +++ b/lib/terralibext.t @@ -0,0 +1,221 @@ +--local io = terralib.includec("stdio.h") + +local function addmissinginit(T) + + --flag that signals that a missing __init method needs to + --be generated + local generate = false + + local runinit = macro(function(self) + local V = self:gettype() + --avoid generating code for empty array initializers + local function hasinit(U) + if U:isstruct() then return U.methods.__init + elseif U:isarray() then return hasinit(U.type) + else return false end + end + if V:isstruct() then + if not V.methods.__init then + addmissinginit(V) + end + local method = V.methods.__init + if method then + generate = true + return quote + self:__init() + end + end + elseif V:isarray() and hasinit(V) then + return quote + var pa = &self + for i = 0,T.N do + runinit((@pa)[i]) + end + end + elseif V:ispointer() then + return quote + self = nil + end + end + return quote end + end) + + local generateinit = macro(function(self) + local T = self:gettype() + local stmts = terralib.newlist() + local entries = T:getentries() + for i,e in ipairs(entries) do + if e.field then + local expr = `runinit(self.[e.field]) + if expr and #expr.tree.statements > 0 then + stmts:insert(expr) + end + end + end + return stmts + end) + + if T:isstruct() and T.methods.__init == nil then + local method = terra(self : &T) + generateinit(@self) + --io.printf("generated __init()\n") + end + if generate then + T.methods.__init = method + else + --set T.methods.__init to false. This means that addmissinginit(T) will not + --attempt to generate 'T.methods.__init' twice + T.methods.__init = false + end + end +end + + +local function addmissingdtor(T) + + --flag that signals that a missing __dtor method needs to + --be generated + local generate = false + + local rundtor = macro(function(self) + local V = self:gettype() + --avoid generating code for empty array destructors + local function hasdtor(U) + if U:isstruct() then return U.methods.__dtor + elseif U:isarray() then return hasdtor(U.type) + else return false end + end + if V:isstruct() then + if not V.methods.__dtor then + addmissingdtor(V) + end + local method = V.methods.__dtor + if method then + generate = true + return quote + self:__dtor() + end + end + elseif V:isarray() and hasdtor(V) then + return quote + var pa = &self + for i = 0,T.N do + rundtor((@pa)[i]) + end + end + end + return quote end + end) + + local generatedtor = macro(function(self) + local T = self:gettype() + local stmts = terralib.newlist() + local entries = T:getentries() + for i,e in ipairs(entries) do + if e.field then + local expr = `rundtor(self.[e.field]) + if expr and #expr.tree.statements > 0 then + stmts:insert(expr) + end + end + end + return stmts + end) + + if T:isstruct() and T.methods.__dtor==nil then + local method = terra(self : &T) + --io.printf("generated __dtor()\n") + generatedtor(@self) + end + if generate then + T.methods.__dtor = method + else + --set T.methods.__dtor to false. This means that addmissingdtor(T) will not + --attempt to generate 'T.methods.__dtor' twice + T.methods.__dtor = false + end + end +end + +local function addmissingcopy(T) + + --flag that signals that a missing __copy method needs to + --be generated + local generate = false + + local runcopy = macro(function(from, to) + local U = from:gettype() + local V = to:gettype() + --avoid generating code for empty array initializers + local function hascopy(W) + if W:isstruct() then return W.methods.__copy + elseif W:isarray() then return hascopy(W.type) + else return false end + end + if V:isstruct() and U==V then + if not V.methods.__copy then + addmissingcopy(V) + end + local method = V.methods.__copy + if method then + generate = true + return quote + method(&from, &to) + end + else + return quote + to = from + end + end + elseif V:isarray() and hascopy(V) then + return quote + var pa = &self + for i = 0,V.N do + runcopy((@pa)[i]) + end + end + else + return quote + to = from + end + end + return quote end + end) + + local generatecopy = macro(function(from, to) + local stmts = terralib.newlist() + local entries = T:getentries() + for i,e in ipairs(entries) do + local field = e.field + if field then + local expr = `runcopy(from.[field], to.[field]) + if expr and #expr.tree.statements > 0 then + stmts:insert(expr) + end + end + end + return stmts + end) + + if T:isstruct() and T.methods.__copy==nil then + local method = terra(from : &T, to : &T) + generatecopy(@from, @to) + --io.printf("generated __copy()\n") + end + if generate then + T.methods.__copy = method + else + --set T.methods.__copy to false. This means that addmissingcopy(T) will not + --attempt to generate 'T.methods.__copy' twice + T.methods.__copy = false + end + end +end + +terralib.ext = { + addmissing = { + __init = addmissinginit, + __dtor = addmissingdtor, + __copy = addmissingcopy + } +} \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1a8e114ea..61e827d8d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ add_custom_command( DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/geninternalizedfiles.lua" "${PROJECT_SOURCE_DIR}/lib/std.t" + "${PROJECT_SOURCE_DIR}/lib/terralibext.t" "${PROJECT_SOURCE_DIR}/lib/parsing.t" LuaJIT COMMAND ${LUAJIT_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/geninternalizedfiles.lua" ${PROJECT_BINARY_DIR}/internalizedfiles.h ${CLANG_RESOURCE_DIR} "%.h$" ${CLANG_RESOURCE_DIR} "%.modulemap$" "${PROJECT_SOURCE_DIR}/lib" "%.t$" diff --git a/src/terralib.lua b/src/terralib.lua index 8ea57c61f..7f1f48628 100644 --- a/src/terralib.lua +++ b/src/terralib.lua @@ -2767,6 +2767,151 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return checkcall(anchor, terra.newlist { fnlike }, fnargs, "first", false, location) end + --check if metamethod is implemented + local function hasmetamethod(v, method) + if not terralib.ext then return false end + local typ = v.type + if typ and typ:isstruct() and typ.methods[method] then + return true + end + return false + end + + --check if methods.__init is implemented + local function checkmetainit(anchor, reciever) + if not terralib.ext then return end + local typ = reciever.type + if typ and typ:isstruct() then + --try to add missing __init method + if not typ.methods.__init then + terralib.ext.addmissing.__init(typ) + end + if typ.methods.__init then + if reciever:is "allocvar" then + reciever = newobject(anchor,T.var,reciever.name,reciever.symbol):setlvalue(true):withtype(typ) + end + return checkmethodwithreciever(anchor, false, "__init", reciever, terralib.newlist(), "statement") + end + end + end + + local function checkmetainitializers(anchor, lhs) + if not terralib.ext then return end + local stmts = terralib.newlist() + for i,e in ipairs(lhs) do + local init = checkmetainit(anchor, e) + if init then + stmts:insert(init) + end + end + return stmts + end + + --check if a __dtor metamethod is implemented for the type corresponding to `sym` + local function checkmetadtor(anchor, reciever) + if not terralib.ext then return end + local typ = reciever.type + if typ and typ:isstruct() then + --try to add missing __dtor method + if not typ.methods.__dtor then + terralib.ext.addmissing.__dtor(typ) + end + if typ.methods.__dtor then + if reciever:is "allocvar" then + reciever = newobject(anchor,T.var,reciever.name,reciever.symbol):setlvalue(true):withtype(typ) + end + return checkmethodwithreciever(anchor, false, "__dtor", reciever, terralib.newlist(), "statement") + end + end + end + + local function checkmetadtors(anchor, stats) + if not terralib.ext then return stats end + --extract the return statement from `stats`, if there is one + local function extractreturnstat() + local n = #stats + if n>0 then + local s = stats[n] + if s:is "returnstat" then + return s + end + end + end + local rstat = extractreturnstat() + --extract the returned `var` symbols from a return statement + local function extractreturnedsymbols() + local ret = {} + --loop over expressions in a `letin` return statement + for i,v in ipairs(rstat.expression.expressions) do + if v:is "var" then + ret[v.name] = v.symbol + end + end + return ret + end + + --get symbols that are returned in case of a return statement + local rsyms = rstat and extractreturnedsymbols() or {} + --get position at which to add destructor statements + local pos = rstat and #stats or #stats+1 + for name,sym in pairs(env:localenv()) do + --if not a return variable ckeck for an implementation of methods.__dtor + if not rsyms[name] then + local reciever = newobject(anchor,T.var, name, sym):setlvalue(true):withtype(sym.type) + local dtor = checkmetadtor(anchor, reciever) + if dtor then + --add deferred calls to the destructors + table.insert(stats, pos, newobject(anchor, T.defer, dtor)) + pos = pos + 1 + end + end + end + return stats + end + + local function checkmetacopyassignment(anchor, from, to) + if not terralib.ext then return end + local ftype, ttype = from.type, to.type + if (ftype and ftype:isstruct()) or (ttype and ttype:isstruct()) then + --case of equal struct types + if ftype == ttype then + if not ftype.methods.__copy then + --try add missing __copy method + terralib.ext.addmissing.__copy(ftype) + end + --if __copy was unsuccessful return to do regular copy + if not (ftype.methods.__copy) then return end + else + --otherwise + if not (hasmetamethod(from, "__copy") or hasmetamethod(to, "__copy")) then return end + end + else + --only struct types are managed + --resort to regular copy + return + end + --if `to` is an allocvar then set type and turn into corresponding `var` + if to:is "allocvar" then + if not to.type then + to:settype(from.type or terra.types.error) + end + to = newobject(anchor,T.var,to.name,to.symbol):setlvalue(true):withtype(to.type) + end + --list of overloaded __copy metamethods + local overloads = terra.newlist() + local function checkoverload(v) + if hasmetamethod(v, "__copy") then + overloads:insert(asterraexpression(anchor, v.type.methods.__copy, "luaobject")) + end + end + --add overloaded methods based on left- and right-hand-side of the assignment + checkoverload(from) + checkoverload(to) + if #overloads > 0 then + return checkcall(anchor, overloads, terralib.newlist{from, to}, "all", true, "expression") + end + end + local function checkmethod(exp, location) local methodname = checklabel(exp.name,true).value assert(type(methodname) == "string" or terra.islabel(methodname)) @@ -3193,7 +3338,48 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return newobject(anchor,T.letin, stmts, List {}, true):withtype(terra.types.unit) end + --divide assignment into regular assignments and copy assignments + local function assignmentkinds(anchor, lhs, rhs) + local regular = {lhs = terralib.newlist(), rhs = terralib.newlist()} + local byfcall = {lhs = terralib.newlist(), rhs = terralib.newlist()} + for i=1,#lhs do + local cpassign = false + --ToDo: for now we call 'checkmetacopyassignment' twice. Refactor with 'createassignment' + local r = rhs[i] + if r then + --alternatively, work on the r.type and check for + --r.type:isprimitive(), r.type:isstruct(), etc + if r:is "operator" and r.operator == "&" then + r = r.operands[1] + end + if r:is "var" or r:is "literal" or r:is "constant" or r:is "select" then + if checkmetacopyassignment(anchor, r, lhs[i]) then + cpassign = true + end + end + end + if cpassign then + --add assignment by __copy call + byfcall.lhs:insert(lhs[i]) + byfcall.rhs:insert(rhs[i]) + else + --default to regular assignment + regular.lhs:insert(lhs[i]) + regular.rhs:insert(rhs[i]) + end + end + if #byfcall.lhs>0 and #byfcall.lhs+#regular.lhs>1 then + --__copy can potentially mutate left and right-handsides in an + --assignment. So we prohibit assignments that may involve something + --like a swap: u,v = v, u. + --for now we prohibit this by limiting such assignments + diag:reporterror(anchor, "assignments of managed objects is not supported for tuples.") + end + return regular, byfcall + end + local function createassignment(anchor,lhs,rhs) + --special case where a rhs struct is unpacked if #lhs > #rhs and #rhs > 0 then local last = rhs[#rhs] if last.type:isstruct() and last.type.convertible == "tuple" and #last.type.entries + #rhs - 1 == #lhs then @@ -3213,25 +3399,102 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return createstatementlist(anchor, List {a1, a2}) end end - local vtypes = lhs:map(function(v) return v.type or "passthrough" end) - rhs = insertcasts(anchor,vtypes,rhs) - for i,v in ipairs(lhs) do - local rhstype = rhs[i] and rhs[i].type or terra.types.error - if v:is "setteru" then - local rv,r = allocvar(v,rhstype,"") - lhs[i] = newobject(v,T.setter, rv,v.setter(r)) - elseif v:is "allocvar" then - v:settype(rhstype) + + if not terralib.ext or #lhs < #rhs then + --an error may be reported later during type-checking: 'expected #lhs parameters (...), but found #rhs (...)' + local vtypes = lhs:map(function(v) return v.type or "passthrough" end) + rhs = insertcasts(anchor, vtypes, rhs) + for i,v in ipairs(lhs) do + local rhstype = rhs[i] and rhs[i].type or terra.types.error + if v:is "setteru" then + local rv,r = allocvar(v,rhstype,"") + lhs[i] = newobject(v,T.setter, rv,v.setter(r)) + elseif v:is "allocvar" then + v:settype(rhstype) + else + ensurelvalue(v) + end + end + return newobject(anchor,T.assignment,lhs,rhs) + else + --standard case #lhs == #rhs + local stmts = terralib.newlist() + local post = terralib.newlist() + --first take care of regular assignments + local regular, byfcall = assignmentkinds(anchor, lhs, rhs) + local vtypes = regular.lhs:map(function(v) return v.type or "passthrough" end) + regular.rhs = insertcasts(anchor, vtypes, regular.rhs) + for i,v in ipairs(regular.lhs) do + local rhstype = regular.rhs[i] and regular.rhs[i].type or terra.types.error + if v:is "setteru" then + local rv,r = allocvar(v,rhstype,"") + regular.lhs[i] = newobject(v,T.setter, rv,v.setter(r)) + elseif v:is "allocvar" then + v:settype(rhstype) + else + ensurelvalue(v) + --if 'v' is a managed variable then + --(1) var tmp = v --store v in tmp + --(2) v = rhs[i] --perform assignment + --(3) tmp:__dtor() --delete old v + --the temporary is necessary because rhs[i] may involve a function of 'v' + if hasmetamethod(v, "__dtor") then + --To avoid unwanted deletions we prohibit assignments that may involve something + --like a swap: u,v = v, u. + --for now we prohibit this by limiting assignments to a single one + if #regular.lhs>1 then + diag:reporterror(anchor, "assignments of managed objects is not supported for tuples.") + end + local tmpa, tmp = allocvar(v, v.type,"") + --store v in tmp + stmts:insert(newobject(anchor,T.assignment, List{tmpa}, List{v})) + --call tmp:__dtor() + post:insert(checkmetadtor(anchor, tmp)) + end + end + end + --take care of copy assignments using methods.__copy + for i,v in ipairs(byfcall.lhs) do + local rhstype = byfcall.rhs[i] and byfcall.rhs[i].type or terra.types.error + if v:is "setteru" then + local rv,r = allocvar(v,rhstype,"") + stmts:insert(checkmetacopyassignment(anchor, byfcall.rhs[i], r)) + stmts:insert(newobject(v,T.setter, rv, v.setter(r))) + elseif v:is "allocvar" then + if not v.type then + v:settype(rhstype) + end + stmts:insert(v) + local init = checkmetainit(anchor, v) + if init then + stmts:insert(init) + end + stmts:insert(checkmetacopyassignment(anchor, byfcall.rhs[i], v)) + else + ensurelvalue(v) + --apply copy assignment - memory resource management is in the + --hands of the programmer + stmts:insert(checkmetacopyassignment(anchor, byfcall.rhs[i], v)) + end + end + if #stmts==0 then + --standard case, no meta-copy-assignments + return newobject(anchor,T.assignment, regular.lhs, regular.rhs) else - ensurelvalue(v) + --managed case using meta-copy-assignments + --the calls to `__copy` are in `stmts` + if #regular.lhs>0 then + stmts:insert(newobject(anchor,T.assignment, regular.lhs, regular.rhs)) + end + stmts:insertall(post) + return createstatementlist(anchor, stmts) end end - return newobject(anchor,T.assignment,lhs,rhs) end function checkblock(s) env:enterblock() - local stats = checkstmts(s.statements) + local stats = checkmetadtors(s, checkstmts(s.statements)) env:leaveblock() return s:copy {statements = stats} end @@ -3314,9 +3577,16 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) elseif s:is "defvar" then local rhs = s.hasinit and checkexpressions(s.initializers) local lhs = checkformalparameterlist(s.variables, not s.hasinit) - local res = s.hasinit and createassignment(s,lhs,rhs) - or createstatementlist(s,lhs) - return res + if s.hasinit then + return createassignment(s,lhs,rhs) + else + local res = createstatementlist(s,lhs) + local ini = checkmetainitializers(s, lhs) + if ini then + res.statements:insertall(ini) + end + return res + end elseif s:is "assignment" then local rhs = checkexpressions(s.rhs) local lhs = checkexpressions(s.lhs,"lexpression") diff --git a/tests/fails/raii-tuple-custom-copy.t b/tests/fails/raii-tuple-custom-copy.t new file mode 100644 index 000000000..5cbabb5a3 --- /dev/null +++ b/tests/fails/raii-tuple-custom-copy.t @@ -0,0 +1,36 @@ +if not require("fail") then return end +require "terralibext" --load 'terralibext' to enable raii + +local std = {} +std.io = terralib.includec("stdio.h") + + +struct A{ + data : int +} + +A.methods.__init = terra(self : &A) + std.io.printf("__init: calling initializer.\n") + self.data = 1 +end + +A.methods.__dtor = terra(self : &A) + std.io.printf("__dtor: calling destructor.\n") + self.data = -1 +end + +A.methods.__copy = terra(from : &A, to : &A) + std.io.printf("__copy: calling custom copy.\n") + to.data = from.data+1 +end + +terra test0() + var a = A{1} + var b = A{2} + a, b = b, a + --tuple assignments are prohibited when __copy is implemented + --because proper resource management cannot be guaranteed + --(at least not yet) + return a.data, b.data +end +test0() \ No newline at end of file diff --git a/tests/fails/raii-tuple-default-copy.t b/tests/fails/raii-tuple-default-copy.t new file mode 100644 index 000000000..fa896ad01 --- /dev/null +++ b/tests/fails/raii-tuple-default-copy.t @@ -0,0 +1,38 @@ +if not require("fail") then return end +require "terralibext" --load 'terralibext' to enable raii +local test = require "test" + +local std = {} +std.io = terralib.includec("stdio.h") + +local function printtestheader(s) + print() + print("===========================") + print(s) + print("===========================") +end + +struct A{ + data : int +} + +A.methods.__init = terra(self : &A) + std.io.printf("__init: calling initializer.\n") + self.data = 1 +end + +A.methods.__dtor = terra(self : &A) + std.io.printf("__dtor: calling destructor.\n") + self.data = -1 +end + +terra test0() + var a = A{1} + var b = A{2} + a, b = b, a --bitcopies don't work in a swap + --tuple assignments are prohibited because proper + --resource management cannot be guaranteed + --(at least not yet) + return a.data, b.data +end +test0() \ No newline at end of file diff --git a/tests/raii-compose.t b/tests/raii-compose.t new file mode 100644 index 000000000..07f1d33de --- /dev/null +++ b/tests/raii-compose.t @@ -0,0 +1,152 @@ +--load 'terralibext' to enable raii +require "terralibext" + +local test = require "test" + +local function printtestheader(s) + print() + print("===========================") + print(s) + print("===========================") +end + +local std = { + io = terralib.includec("stdio.h") +} + +--A is a managed struct, as it implements __init, __copy, __dtor +local struct A{ + data : int +} + +A.methods.__init = terra(self : &A) + std.io.printf("A.__init\n") + self.data = 1 +end + +A.methods.__dtor = terra(self : &A) + std.io.printf("A.__dtor\n") + self.data = -1 +end + +A.methods.__copy = terra(from : &A, to : &A) + std.io.printf("A.__copy\n") + to.data = from.data + 2 +end + +local struct B{ + data : int +} + +local struct C{ + data_a : A --managed + data_b : B --not managed +} + +local struct D{ + data_a : A --managed + data_b : B --not managed + data_c : C +} + +printtestheader("raii-compose.t - testing __init for managed struct") +local terra testinit_A() + var a : A + return a.data +end +test.eq(testinit_A(), 1) + +printtestheader("raii-compose.t - testing __init for managed field") +local terra testinit_C() + var c : C + return c.data_a.data +end +test.eq(testinit_C(), 1) + +printtestheader("raii-compose.t - testing __init for managed field and subfield") +local terra testinit_D() + var d : D + return d.data_a.data + d.data_c.data_a.data +end +test.eq(testinit_D(), 2) + +printtestheader("raii-compose.t - testing __dtor for managed struct") +local terra testdtor_A() + var x : &int + do + var a : A + x = &a.data + end + return @x +end +test.eq(testdtor_A(), -1) + +printtestheader("raii-compose.t - testing __dtor for managed field") +local terra testdtor_C() + var x : &int + do + var c : C + x = &c.data_a.data + end + return @x +end +test.eq(testdtor_C(), -1) + +printtestheader("raii-compose.t - testing __dtor for managed field and subfield") +local terra testdtor_D() + var x : &int + var y : &int + do + var d : D + x = &d.data_a.data + y = &d.data_c.data_a.data + end + return @x + @y +end +test.eq(testdtor_D(), -2) + +printtestheader("raii-compose.t - testing __copy for managed field") +terra testcopyassignment_C() + var c_1 : C + var c_2 : C + c_1.data_a.data = 5 + c_2 = c_1 + std.io.printf("value c_2._data_a.data %d\n", c_2.data_a.data) + return c_2.data_a.data +end +test.eq(testcopyassignment_C(), 5 + 2) + +printtestheader("raii-compose.t - testing __copy for managed field and subfield") +terra testcopyassignment_D() + var d_1 : D + var d_2 : D + d_1.data_a.data = 5 + d_1.data_c.data_a.data = 6 + d_2 = d_1 + std.io.printf("value d_2._data_a.data %d\n", d_2.data_a.data) + std.io.printf("value d_2._data_c.data_a.data %d\n", d_2.data_c.data_a.data) + return d_2.data_a.data + d_2.data_c.data_a.data +end +test.eq(testcopyassignment_D(), 5 + 2 + 6 + 2) + +printtestheader("raii-compose.t - testing __copy construction for managed field") +terra testcopyconstruction_C() + var c_1 : C + c_1.data_a.data = 5 + var c_2 : C = c_1 + std.io.printf("value c_2._data_a.data %d\n", c_2.data_a.data) + return c_2.data_a.data +end +test.eq(testcopyconstruction_C(), 5 + 2) + +printtestheader("raii-compose.t - testing __copy construction for managed field and subfield") +terra testcopyconstruction_D() + var d_1 : D + d_1.data_a.data = 5 + d_1.data_c.data_a.data = 6 + var d_2 = d_1 + std.io.printf("value d_2._data_a.data %d\n", d_2.data_a.data) + std.io.printf("value d_2._data_c.data_a.data %d\n", d_2.data_c.data_a.data) + return d_2.data_a.data + d_2.data_c.data_a.data +end +test.eq(testcopyconstruction_D(), 5 + 2 + 6 + 2) diff --git a/tests/raii-copyctr-cast.t b/tests/raii-copyctr-cast.t new file mode 100644 index 000000000..ca31c9031 --- /dev/null +++ b/tests/raii-copyctr-cast.t @@ -0,0 +1,88 @@ +require "terralibext" --load 'terralibext' to enable raii +--[[ + We need that direct initialization + var a : A = b + yields the same result as + var a : A + a = b + If 'b' is a variable or a literal (something with a value) and the user has + implemented the right copy-assignment 'A.methods.__copy' then the copy + should be performed using this method. + If the method is not implemented for the exact types then a (user-defined) + implicit cast should be attempted. +--]] + +local test = require("test") +local std = { + io = terralib.includec("stdio.h") +} + +struct A{ + data : int +} + +A.methods.__init = terra(self : &A) + std.io.printf("__init: calling initializer.\n") + self.data = 1 +end + +A.methods.__dtor = terra(self : &A) + std.io.printf("__dtor: calling destructor.\n") + self.data = -1 +end + +A.methods.__copy = terra(from : &A, to : &A) + std.io.printf("__copy: calling copy assignment {&A, &A} -> {}.\n") + to.data = to.data + from.data + 10 +end + +A.metamethods.__cast = function(from, to, exp) + print("attempting cast from "..tostring(from).." --> "..tostring(to)) + if to == &A and from:ispointer() then + return quote + var tmp = A{@exp} + in + &tmp + end + end +end + +--[[ + The integer '2' will first be cast to a temporary of type A using + the user defined A.metamethods.__cast method. Then the method + A.methods.__init(self : &A) is called to initialize the variable + and then the copy-constructor A.methods.__copy(from : &A, to : &A) + will be called to finalize the copy-construction. +--]] +terra testwithcast() + var a : A = 2 + return a.data +end + +-- to.data + from.data + 10 = 1 + 2 + 10 = 13 +test.eq(testwithcast(), 13) + + +A.methods.__copy = terralib.overloadedfunction("__copy") + +A.methods.__copy:adddefinition(terra(from : &A, to : &A) + std.io.printf("__copy: calling copy assignment {&A, &A} -> {}.\n") + to.data = to.data + from.data + 10 +end) + +A.methods.__copy:adddefinition(terra(from : int, to : &A) + std.io.printf("__copy: calling copy assignment {int, &A} -> {}.\n") + to.data = to.data + from + 11 +end) + +--[[ + The method A.methods.__init(self : &A) is called to initialize the variable + and then the copy-constructor A.methods.__copy(from : int, to : &A) will be + called to finalize the copy-construction. +--]] +terra testwithoutcast() + var a : A = 2 + return a.data +end +-- to.data + from.data + 10 = 1 + 2 + 11 = 14 +test.eq(testwithoutcast(), 14) \ No newline at end of file diff --git a/tests/raii-offset_ptr.t b/tests/raii-offset_ptr.t new file mode 100644 index 000000000..a06cdbf87 --- /dev/null +++ b/tests/raii-offset_ptr.t @@ -0,0 +1,37 @@ +require "terralibext" --load 'terralibext' to enable raii +local test = require("test") + +local std = { + io = terralib.includec("stdio.h") +} + +struct offset_ptr{ + offset : int + init : bool +} + +offset_ptr.methods.__copy = terra(from : &int64, to : &offset_ptr) + to.offset = [&int8](from) - [&int8](to) + to.init = true + std.io.printf("offset_ptr: __copy &int -> &offset_ptr\n") +end + +local struct A{ + integer_1 : int64 + integer_2 : int64 + ptr : offset_ptr +} + +terra test0() + var a : A + a.ptr = &a.integer_1 + var save_1 = a.ptr.offset + std.io.printf("value of a.ptr.offset: %d\n", a.ptr.offset) + a.ptr = &a.integer_2 + var save_2 = a.ptr.offset + std.io.printf("value of a.ptr.offset: %d\n", a.ptr.offset) + return save_1, save_2 +end + +--test the offset in bytes between ptr and the integers in struct A +test.meq({-16, -8},test0()) \ No newline at end of file diff --git a/tests/raii-shared_ptr.t b/tests/raii-shared_ptr.t new file mode 100644 index 000000000..18ab3b56b --- /dev/null +++ b/tests/raii-shared_ptr.t @@ -0,0 +1,142 @@ +--load 'terralibext' to enable raii +require "terralibext" +local test = require("test") + +local std = {} +std.io = terralib.includec("stdio.h") +std.lib = terralib.includec("stdlib.h") + +local function printtestdescription(s) + print() + print("======================================") + print(s) + print("======================================") +end + +--implementation of a smart (shared) pointer type +local function SharedPtr(T) + + local struct A{ + data : &T --underlying data ptr (reference counter is stored in its head) + } + + --table for static methods + local static_methods = {} + + A.metamethods.__getmethod = function(self, methodname) + return A.methods[methodname] or static_methods[methodname] or error("No method " .. methodname .. "defined on " .. self) + end + + A.methods.refcounter = terra(self : &A) + if self.data ~= nil then + return ([&int8](self.data))-1 + end + return nil + end + + A.methods.increaserefcounter = terra(self : &A) + var ptr = self:refcounter() + if ptr ~= nil then + @ptr = @ptr+1 + end + end + + A.methods.decreaserefcounter = terra(self : &A) + var ptr = self:refcounter() + if ptr ~= nil then + @ptr = @ptr-1 + end + end + + A.methods.__dereference = terra(self : &A) + return @self.data + end + + static_methods.new = terra() + std.io.printf("new: allocating memory. start\n") + defer std.io.printf("new: allocating memory. return.\n") + --heap allocation for `data` with the reference counter `refcount` stored in its head and the real data in its tail + var head = sizeof(int8) + var tail = sizeof(T) + var ptr = [&int8](std.lib.malloc(head+tail)) + --assign to data + var x = A{[&T](ptr+1)} + --initializing the reference counter to one + @x:refcounter() = 1 + return x + end + + A.methods.__init = terra(self : &A) + std.io.printf("__init: initializing object\n") + self.data = nil -- initialize data pointer to nil + std.io.printf("__init: initializing object. return.\n") + end + + A.methods.__dtor = terra(self : &A) + std.io.printf("__dtor: calling destructor. start\n") + defer std.io.printf("__dtor: calling destructor. return\n") + --if uninitialized then do nothing + if self.data == nil then + return + end + --the reference counter is `nil`, `1` or `> 1`. + if @self:refcounter() == 1 then + --free memory if the last shared pointer obj runs out of life + std.io.printf("__dtor: reference counter: %d -> %d.\n", @self:refcounter(), @self:refcounter()-1) + std.io.printf("__dtor: free'ing memory.\n") + std.lib.free(self:refcounter()) + self.data = nil --reinitialize data ptr + else + --otherwise reduce reference counter + self:decreaserefcounter() + std.io.printf("__dtor: reference counter: %d -> %d.\n", @self:refcounter()+1, @self:refcounter()) + end + end + + A.methods.__copy = terra(from : &A, to : &A) + std.io.printf("__copy: calling copy-assignment operator. start\n") + defer std.io.printf("__copy: calling copy-assignment operator. return\n") + to.data = from.data + to:increaserefcounter() + end + + --return parameterized shared pointer type + return A +end + +local shared_ptr_int = SharedPtr(int) + +printtestdescription("shared_ptr - copy construction.") +local terra test0() + var a : shared_ptr_int + std.io.printf("main: a.refcount: %p\n", a:refcounter()) + a = shared_ptr_int.new() + @a.data = 10 + std.io.printf("main: a.data: %d\n", @a.data) + std.io.printf("main: a.refcount: %d\n", @a:refcounter()) + var b = a + std.io.printf("main: b.data: %d\n", @b.data) + std.io.printf("main: a.refcount: %d\n", @a:refcounter()) + if a:refcounter()==b:refcounter() then + return @b.data * @a:refcounter() --10 * 2 + end +end +test.eq(test0(), 20) + +printtestdescription("shared_ptr - copy assignment.") +local terra test1() + var a : shared_ptr_int, b : shared_ptr_int + std.io.printf("main: a.refcount: %p\n", a:refcounter()) + a = shared_ptr_int.new() + @a.data = 11 + std.io.printf("main: a.data: %d\n", @a.data) + std.io.printf("main: a.refcount: %d\n", @a:refcounter()) + b = a + std.io.printf("main: b.data: %d\n", @b.data) + std.io.printf("main: a.refcount: %d\n", @a:refcounter()) + if a:refcounter()==b:refcounter() then + return @b.data * @a:refcounter() --11 * 2 + end +end +test.eq(test1(), 22) + diff --git a/tests/raii-unique_ptr.t b/tests/raii-unique_ptr.t new file mode 100644 index 000000000..7275e5cb0 --- /dev/null +++ b/tests/raii-unique_ptr.t @@ -0,0 +1,97 @@ +require "terralibext" --load 'terralibext' to enable raii + +local std = { + io = terralib.includec("stdio.h"), + lib = terralib.includec("stdlib.h") +} + +local function printtestheader(s) + print() + print("===========================") + print(s) + print("===========================") +end + +struct A{ + data : &int + heap : bool +} + +A.methods.__init = terra(self : &A) + std.io.printf("__init: initializing object. start.\n") + self.data = nil -- initialize data pointer to nil + self.heap = false --flag to denote heap resource + std.io.printf("__init: initializing object. return.\n") +end + +A.methods.__dtor = terra(self : &A) + std.io.printf("__dtor: calling destructor. start\n") + defer std.io.printf("__dtor: calling destructor. return\n") + if self.heap then + std.lib.free(self.data) + self.data = nil + self.heap = false + std.io.printf("__dtor: freed memory.\n") + end +end + +A.methods.__copy = terralib.overloadedfunction("__copy") +A.methods.__copy:adddefinition( +terra(from : &A, to : &A) + std.io.printf("__copy: moving resources {&A, &A} -> {}.\n") + to.data = from.data + to.heap = from.heap + from.data = nil + from.heap = false +end) +A.methods.__copy:adddefinition( +terra(from : &int, to : &A) + std.io.printf("__copy: assignment {&int, &A} -> {}.\n") + to.data = from + to.heap = false --not known at compile time +end) + +--dereference ptr +terra A.methods.getvalue(self : &A) + return @self.data +end + +terra A.methods.setvalue(self : &A, value : int) + @self.data = value +end + +--heap memory allocation +terra A.methods.allocate(self : &A) + std.io.printf("allocate: allocating memory. start\n") + defer std.io.printf("allocate: allocating memory. return.\n") + self.data = [&int](std.lib.malloc(sizeof(int))) + self.heap = true +end + + +local test = require "test" + +printtestheader("raii-unique_ptr.t: test return ptr value from function before resource is deleted") +terra testdereference() + var ptr : A + ptr:allocate() + ptr:setvalue(3) + return ptr:getvalue() +end +test.eq(testdereference(), 3) + +terra returnheapresource() + var ptr : A + ptr:allocate() + ptr:setvalue(3) + return ptr +end + +printtestheader("raii-unique_ptr.t: test return heap resource from function") +terra testgetptr() + var ptr = returnheapresource() + return ptr:getvalue() +end +test.eq(testgetptr(), 3) + + diff --git a/tests/raii.t b/tests/raii.t new file mode 100644 index 000000000..96542c6ae --- /dev/null +++ b/tests/raii.t @@ -0,0 +1,96 @@ +require "terralibext" --load 'terralibext' to enable raii +local test = require "test" + +local std = { + io = terralib.includec("stdio.h") +} + +local function printtestheader(s) + print() + print("===========================") + print(s) + print("===========================") +end + +struct A{ + data : int +} + +A.methods.__init = terra(self : &A) + std.io.printf("__init: calling initializer.\n") + self.data = 1 +end + +A.methods.__dtor = terra(self : &A) + std.io.printf("__dtor: calling destructor.\n") + self.data = -1 +end + +A.methods.__copy = terralib.overloadedfunction("__copy") +A.methods.__copy:adddefinition(terra(from : &A, to : &A) + std.io.printf("__copy: calling copy assignment {&A, &A} -> {}.\n") + to.data = from.data + 10 +end) +A.methods.__copy:adddefinition(terra(from : int, to : &A) + std.io.printf("__copy: calling copy assignment {int, &A} -> {}.\n") + to.data = from +end) +A.methods.__copy:adddefinition(terra(from : &A, to : &int) + std.io.printf("__copy: calling copy assignment {&A, &int} -> {}.\n") + @to = from.data +end) + + +printtestheader("raii.t - testing __init metamethod") +terra testinit() + var a : A + return a.data +end +test.eq(testinit(), 1) + +printtestheader("raii.t - testing __dtor metamethod") +terra testdtor() + var x : &int + do + var a : A + x = &a.data + end + return @x +end +test.eq(testdtor(), -1) + +printtestheader("raii.t - testing __copy metamethod in copy-construction") +terra testcopyconstruction() + var a : A + var b = a + return b.data +end +test.eq(testcopyconstruction(), 11) + +printtestheader("raii.t - testing __copy metamethod in copy-assignment") +terra testcopyassignment() + var a : A + a.data = 2 + var b : A + b = a + return b.data +end +test.eq(testcopyassignment(), 12) + +printtestheader("raii.t - testing __copy metamethod in copy-assignment from integer to struct.") +terra testcopyassignment1() + var a : A + a = 3 + return a.data +end +test.eq(testcopyassignment1(), 3) + +printtestheader("raii.t - testing __copy metamethod in copy-assignment from struct to integer.") +terra testcopyassignment2() + var a : A + var x : int + a.data = 5 + x = a + return x +end +test.eq(testcopyassignment2(), 5) \ No newline at end of file