From 56349d673827733069a38776e93a03df298782ee Mon Sep 17 00:00:00 2001 From: rrhiemstar Date: Wed, 27 Nov 2024 12:44:28 +0100 Subject: [PATCH 1/9] added unit tests for RAII __dtor, __copy, and __init functionality. --- tests/fails/raii-tuple-custom-copy.t | 36 ++++++ tests/fails/raii-tuple-default-copy.t | 38 +++++++ tests/raii-compose.t | 152 ++++++++++++++++++++++++++ tests/raii-copyctr-cast.t | 89 +++++++++++++++ tests/raii-copyctr.t | 57 ++++++++++ tests/raii-offset_ptr.t | 37 +++++++ tests/raii-shared_ptr.t | 142 ++++++++++++++++++++++++ tests/raii-unique_ptr.t | 97 ++++++++++++++++ tests/raii.t | 126 +++++++++++++++++++++ 9 files changed, 774 insertions(+) create mode 100644 tests/fails/raii-tuple-custom-copy.t create mode 100644 tests/fails/raii-tuple-default-copy.t create mode 100644 tests/raii-compose.t create mode 100644 tests/raii-copyctr-cast.t create mode 100644 tests/raii-copyctr.t create mode 100644 tests/raii-offset_ptr.t create mode 100644 tests/raii-shared_ptr.t create mode 100644 tests/raii-unique_ptr.t create mode 100644 tests/raii.t 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..d08c09ae0 --- /dev/null +++ b/tests/raii-copyctr-cast.t @@ -0,0 +1,89 @@ +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 + tmp.data = @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-copyctr.t b/tests/raii-copyctr.t new file mode 100644 index 000000000..01657be95 --- /dev/null +++ b/tests/raii-copyctr.t @@ -0,0 +1,57 @@ +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. +--]] + +local test = require("test") +io = terralib.includec("stdio.h") + +struct A{ + data : int +} + +A.methods.__init = terra(self : &A) + io.printf("__init: calling initializer.\n") + self.data = 1 +end + +A.methods.__dtor = terra(self : &A) + io.printf("__dtor: calling destructor.\n") + self.data = -1 +end + +A.methods.__copy = terra(from : &A, to : &A) + io.printf("__copy: calling copy assignment {&A, &A} -> {}.\n") + to.data = to.data + from.data + 1 +end + +terra test1() + var a : A --__init -> a.data = 1 + var aa = a --__init + __copy -> a.data = 3 + return aa.data +end + +-- aa.data = aa.data + a.data + 1 = 3 +test.eq(test1(), 3) + +--since A is managed, an __init, __dtor, and __copy will +--be generated +struct B{ + data : A +} + +terra test2() + var a : A --__init -> a.data = 1 + var b = B{a} --__init + __copy -> b.data.data = 3 + return b.data.data +end + +-- b.data.data = b.data.data + a.data + 1 = 3 +test.eq(test2(), 3) \ 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..8f6fed6a3 --- /dev/null +++ b/tests/raii.t @@ -0,0 +1,126 @@ +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) + +--generate implementation for __move +terralib.ext.addmissing.__move(A) + +printtestheader("raii.t - return from function.") +terra returnone() + var a = A{4}:__move() --call __move to move resources into 'a' (make sure copy constructor is not called) + return a +end + +terra testreturnfromfun1() + var a = returnone() + return a.data +end +test.eq(testreturnfromfun1(), 4) + +printtestheader("raii.t - return tuple from function.") + +terra returntwo() + var a = A{4}:__move() --call __move to move resources into 'a' (make sure copy constructor is not called) + var b = A{5}:__move() --same for 'b' + return a, b +end + +terra testreturnfromfun2() + var a, b = returntwo() + return a.data * b.data +end +print(testreturnfromfun2()) +test.eq(testreturnfromfun2(), 20) \ No newline at end of file From 81e419402a5ec6db678e4bcba2006a2f10f3f962 Mon Sep 17 00:00:00 2001 From: rrhiemstar Date: Wed, 27 Nov 2024 12:47:46 +0100 Subject: [PATCH 2/9] added 'terralibext.t' which provides code generation for the RAII implementation in 'terralib.lua' --- lib/terralibext.t | 306 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 lib/terralibext.t diff --git a/lib/terralibext.t b/lib/terralibext.t new file mode 100644 index 000000000..2ee8e3f90 --- /dev/null +++ b/lib/terralibext.t @@ -0,0 +1,306 @@ +--__forward takes a value by reference and simply forwards it by reference, +--creating an rvalue +local function addmissingforward(T) + if T:isstruct() then + if T.methods.__forward then + T.methods.__forward_generated = T.methods.__forward + return + end + if not T.methods.__forward and not T.methods.__forward_generated then + T.methods.__forward_generated = terra(self : &T) + return self --simply forward the variable (turning it into an rvalue) + end + T.methods.__forward = T.methods.__forward_generated + return + end + end +end + +--__create a missing __init for 'T' and all its entries +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() then + --__init is implemented + if T.methods.__init and not T.methods.__init_generated then + T.methods.__init_generated = T.methods.__init + return + end + --__dtor is not implemented + if not T.methods.__init and not T.methods.__init_generated then + T.methods.__init_generated = terra(self : &T) + generateinit(@self) + end + if generate then + T.methods.__init = T.methods.__init_generated + 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 + return + end + end +end + +--__create a missing __dtor for 'T' and all its entries +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() then + --__dtor is implemented + if T.methods.__dtor and not T.methods.__dtor_generated then + T.methods.__dtor_generated = T.methods.__dtor + return + end + --__dtor is not implemented + if not T.methods.__dtor and not T.methods.__dtor_generated then + --generate __dtor + T.methods.__dtor_generated = terra(self : &T) + generatedtor(@self) + end + if generate then + T.methods.__dtor = T.methods.__dtor_generated + 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 + return + end + end +end + +--__create a missing __copy for 'T' and all its entries +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() then + --__copy is implemented + if T.methods.__copy and not T.methods.__copy_generated then + T.methods.__copy_generated = T.methods.__copy + return + end + --__copy is not implemented + if not T.methods.__copy and not T.methods.__copy_generated then + --generate __copy + T.methods.__copy_generated = terra(from : &T, to : &T) + generatecopy(@from, @to) + end + if generate then + T.methods.__copy = T.methods.__copy_generated + 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 + return + end + end +end + +--generate __move, which moves resources to a new allocated variable +local function addmissingmove(T) + if T:isstruct() then + if T.methods.__move and not T.methods.__move_generated then + T.methods.__move_generated = T.methods.__move + return + end + + if not T.methods.__move and not T.methods.__move_generated then + --generate missing __forward and __init + addmissingforward(T) + addmissinginit(T) + --if an __init was generated then we can generate a specialized __move + if T.methods.__init then + T.methods.__move_generated = terra(self : &T) + var new = @self:__forward_generated() --shallow copy of 'self' + self:__init_generated() --initialize old 'self' + return new + end + T.methods.__move = T.methods.__move_generated + --otherwise, __move is just __forward and is accessible only in __move_generated + else + T.methods.__move_generated = T.methods.__forward_generated + T.methods.__move = false + end + return + end + end +end + +local function addmissingraii(T) + addmissingforward(T) + addmissingdinit(T) + addmissingdtor(T) + addmissingcopy(T) + addmissingmove(T) +end + +terralib.ext = { + addmissing = { + __forward = addmissingforward, + __init = addmissinginit, + __dtor = addmissingdtor, + __copy = addmissingcopy, + __move = addmissingmove, + __all = addmissingraii + } +} \ No newline at end of file From e008316f9fce9eb60b886636a51109a9622e4fc2 Mon Sep 17 00:00:00 2001 From: rrhiemstar Date: Wed, 27 Nov 2024 14:58:01 +0100 Subject: [PATCH 3/9] added typechecking of raii methods__init, __copy, and __dtor --- src/terralib.lua | 125 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/terralib.lua b/src/terralib.lua index 8ea57c61f..9b66747ff 100644 --- a/src/terralib.lua +++ b/src/terralib.lua @@ -2767,6 +2767,131 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return checkcall(anchor, terra.newlist { fnlike }, fnargs, "first", false, location) end + --check if raii method is implemented, does not generate one + local function hasraiimethod(receiver, method) + if not terralib.ext then return false end + local typ = receiver.type + if typ and typ:isstruct() and typ.methods[method] then + return true + end + return false + end + + --check if raii method is implemented and generates one using `terralibext.t` if it is missing + local function ismanaged(receiver, method) + if not terralib.ext then return false end + local typ = receiver.type + if typ and typ:isstruct() then + terralib.ext.addmissing[method](typ) + if typ.methods[method] then + return true + end + end + return false + end + + --type check raii method __init or __dtor. __copy is handled separately + --methods are generated if they are missing + local function checkraiimethodwithreceiver(anchor, reciever, method) + if not terralib.ext then return end + if ismanaged(reciever, method) then + if reciever:is "allocvar" then + reciever = newobject(anchor,T.var,reciever.name,reciever.symbol):setlvalue(true):withtype(reciever.type) + end + return checkmethodwithreciever(anchor, false, method, reciever, terralib.newlist(), "statement") + end + end + + --generate and typecheck raii __init's for use in a 'defvar' statement + local function checkraiiinitializers(anchor, lhs) + if not terralib.ext then return end + local stmts = terralib.newlist() + for i,e in ipairs(lhs) do + local init = checkraiimethodwithreceiver(anchor, e, "__init") + if init then + stmts:insert(init) + end + end + return stmts + end + + --generate and typecheck raii __dtor's for use in `block` (scope) statement + local function checkraiidtors(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, then check 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 = checkraiimethodwithreceiver(anchor, reciever, "__dtor") + 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 + + --type check raii __copy (copy-assignment) methods. They are generated + --if they are missing. + local function checkraiicopyassignment(anchor, from, to) + if not terralib.ext then return end + --check for 'from.type.methods.__copy' and 'to.type.methods.__copy' and generate them + --if needed + if not (ismanaged(from, "__copy") or ismanaged(to, "__copy")) then + --return early in case types are not managed and + --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 hasraiimethod(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)) From f4120495563c109b687d663cf9ca042850f9ed2d Mon Sep 17 00:00:00 2001 From: rrhiemstar Date: Wed, 27 Nov 2024 15:00:12 +0100 Subject: [PATCH 4/9] added destructor calls to end of scope of code block --- src/terralib.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/terralib.lua b/src/terralib.lua index 9b66747ff..d1e74e397 100644 --- a/src/terralib.lua +++ b/src/terralib.lua @@ -2858,7 +2858,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) end return stats end - + --type check raii __copy (copy-assignment) methods. They are generated --if they are missing. local function checkraiicopyassignment(anchor, from, to) @@ -3354,9 +3354,10 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return newobject(anchor,T.assignment,lhs,rhs) end + --check block statements and generate and typecheck raii __dtor's function checkblock(s) env:enterblock() - local stats = checkstmts(s.statements) + local stats = checkraiidtors(s, checkstmts(s.statements)) env:leaveblock() return s:copy {statements = stats} end From 7b0b1d3c1916b1ff18d2e22d820ab54f4d575fd9 Mon Sep 17 00:00:00 2001 From: rrhiemstar Date: Wed, 27 Nov 2024 15:02:33 +0100 Subject: [PATCH 5/9] added calls to __init in defvar statements. --- src/terralib.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/terralib.lua b/src/terralib.lua index d1e74e397..d1271a40a 100644 --- a/src/terralib.lua +++ b/src/terralib.lua @@ -3440,9 +3440,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 = checkraiiinitializers(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") From fc3de167815d3e5c05f951c9e25e78081a85c0fb Mon Sep 17 00:00:00 2001 From: rrhiemstar Date: Wed, 27 Nov 2024 15:06:08 +0100 Subject: [PATCH 6/9] refactored createassignment --- src/terralib.lua | 52 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/src/terralib.lua b/src/terralib.lua index d1271a40a..9945472b5 100644 --- a/src/terralib.lua +++ b/src/terralib.lua @@ -3318,26 +3318,46 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return newobject(anchor,T.letin, stmts, List {}, true):withtype(terra.types.unit) end + --struct assignment pattern matching applies? true / false + local function patterncanbematched(lhs, rhs) + local last = rhs[#rhs] + if last.type:isstruct() and last.type.convertible == "tuple" and #last.type.entries + #rhs - 1 == #lhs then + return true + end + return false + end + + local createassignment, createregularassignment + + --try unpack struct and perform pattern match + local function trystructpatternmatching(anchor, lhs, rhs) + local last = rhs[#rhs] + local av,v = allocvar(anchor,last.type,"") --temporary variable "" + local newlhs,lhsp,rhsp = terralib.newlist(),terralib.newlist(),terralib.newlist() + for i,l in ipairs(lhs) do + if i < #rhs then + newlhs:insert(l) + else + lhsp:insert(l) + rhsp:insert((insertselect(v,"_"..tostring(i - #rhs)))) + end + end + newlhs[#rhs] = av + local a1 = createassignment(anchor, newlhs, rhs) --potential managed assignment + local a2 = createregularassignment(anchor, lhsp, rhsp) --regular assignment - __copy and __dtor are possibly already called in 'a1' + return createstatementlist(anchor, List {a1, a2}) + end + + --create regular assignment - no managed types 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 - --struct pattern match - local av,v = allocvar(anchor,last.type,"") - local newlhs,lhsp,rhsp = terralib.newlist(),terralib.newlist(),terralib.newlist() - for i,l in ipairs(lhs) do - if i < #rhs then - newlhs:insert(l) - else - lhsp:insert(l) - rhsp:insert((insertselect(v,"_"..tostring(i - #rhs)))) - end - end - newlhs[#rhs] = av - local a1,a2 = createassignment(anchor,newlhs,rhs), createassignment(anchor,lhsp,rhsp) - return createstatementlist(anchor, List {a1, a2}) + if patterncanbematched(lhs, rhs) then + return trystructpatternmatching(anchor, lhs, rhs) end end + --if #lhs~=#rhs 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 From dc68a49bd9940d670c4c6ddc684f0cfbc492625b Mon Sep 17 00:00:00 2001 From: rrhiemstar Date: Wed, 27 Nov 2024 15:09:53 +0100 Subject: [PATCH 7/9] added regular / managed assignments. --- src/terralib.lua | 138 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/src/terralib.lua b/src/terralib.lua index 9945472b5..90914a8e3 100644 --- a/src/terralib.lua +++ b/src/terralib.lua @@ -3318,6 +3318,46 @@ 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 divideintoregularandmanagedassignment(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 'checkraiicopyassignment' 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" or r:is "structcast" then + if checkraiicopyassignment(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 + --struct assignment pattern matching applies? true / false local function patterncanbematched(lhs, rhs) local last = rhs[#rhs] @@ -3349,7 +3389,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) end --create regular assignment - no managed types - local function createassignment(anchor,lhs,rhs) + function createregularassignment(anchor,lhs,rhs) --special case where a rhs struct is unpacked if #lhs > #rhs and #rhs > 0 then if patterncanbematched(lhs, rhs) then @@ -3374,6 +3414,102 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return newobject(anchor,T.assignment,lhs,rhs) end + --create unmanaged/managed regular or copy assignments + local function createmanagedassignment(anchor, lhs, rhs) + --special case where a rhs struct is unpacked + if #lhs > #rhs and #rhs > 0 then + if patterncanbematched(lhs, rhs) then + return trystructpatternmatching(anchor, lhs, rhs) + end + end + --sanity check + assert(#lhs == #rhs) + --standard case #lhs == #rhs + local stmts, post = terralib.newlist(), terralib.newlist() + --first take care of regular assignments + local regular, byfcall = divideintoregularandmanagedassignment(anchor, lhs, rhs) + local vtypes = regular.lhs:map(function(v) return v.type or "passthrough" end) + regular.rhs = insertcasts(anchor, vtypes, regular.rhs) + --take care of regular assignments of managed variables + 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 ismanaged(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(checkraiimethodwithreceiver(anchor, tmp, "__dtor")) + 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(checkraiicopyassignment(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 = checkraiimethodwithreceiver(anchor, v, "__init") + if init then + stmts:insert(init) + end + stmts:insert(checkraiicopyassignment(anchor, byfcall.rhs[i], v)) + else + ensurelvalue(v) + --apply copy assignment - memory resource management is in the + --hands of the programmer + stmts:insert(checkraiicopyassignment(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 + --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 + + --create assignment - regular / copy assignment + function createassignment(anchor, lhs, rhs) + if not terralib.ext or #lhs < #rhs then + --regular assignment - __init, __copy and __dtor will not be scheduled + return createregularassignment(anchor, lhs, rhs) + else + --managed assignment - __init, __copy and __dtor are scheduled for managed + --variables + return createmanagedassignment(anchor, lhs, rhs) + end + end + --check block statements and generate and typecheck raii __dtor's function checkblock(s) env:enterblock() From 87f766ec3c80f79826e454e85df606261f07e971 Mon Sep 17 00:00:00 2001 From: rrhiemstar Date: Wed, 27 Nov 2024 15:25:30 +0100 Subject: [PATCH 8/9] added documentation on raii feature. --- docs/raii.md | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 docs/raii.md diff --git a/docs/raii.md b/docs/raii.md new file mode 100644 index 000000000..39d588f29 --- /dev/null +++ b/docs/raii.md @@ -0,0 +1,177 @@ +# Deterministic Automated Resource Management + +Resource management in programming languages generally falls into one of the following categories: + +1. **Manual allocation and deallocation** +2. **Automatic garbage collection** +3. **Automatic scope-bound resource management** (commonly referred to as [RAII](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization), or *Resource Acquisition Is Initialization*). + +Traditionally, Terra has only supported manual, C-style resource management. While functional, this approach limits the full potential of Terra’s powerful metaprogramming capabilities. To address this limitation, the current implementation introduces **automated resource management**. + +--- + +## Scope-Bound Resource Management (RAII) + +The new implementation provides **scope-bound resource management (RAII)**, a method typically associated with systems programming languages like C++ and Rust. With RAII, a resource's lifecycle is tied to the stack object that manages it. When the object goes out of scope and is not explicitly returned, the associated resource is automatically destructed. + +### Examples of Resources Managed via RAII: +- Allocated heap memory +- Threads of execution +- Open sockets +- Open files +- Locked mutexes +- Disk space +- Database connections + +--- + +## Experimental Implementation Overview + +The current Terra implementation supports the **Big Three** (as described by the [Rule of Three](https://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming)) in C++): + +1. **Object destruction** +2. **Copy assignment** +3. **Copy construction** + +However, **rvalue references** (introduced in C++11) are not supported in Terra. As a result, the current RAII implementation is comparable to that of **C++03** or **Rust**. + +### Key Features: +Compiler support is provided for the following methods: +```terra +A.methods.__init(self : &A) +A.methods.__dtor(self : &A) +(A or B).methods.__copy(from : &A, to : &B) +``` +These methods facilitate the implementation of smart containers and pointers, such as `std::string`, `std::vector` and `std::unique_ptr`, `std::shared_ptr`, `boost:offset_ptr` in C++. + +### Design Overview +* No Breaking Changes: This implementation does not introduce breaking changes to existing Terra code. No new keywords are required, ensuring that existing syntax remains compatible. +* Type Checking Integration: These methods are introduced during the type-checking phase (handled in terralib.lua). They can be implemented as either macros or Terra functions. +* Composability: The implementation follows simple, consistent rules that ensure smooth compatibility with existing Terra syntax for construction, casting, and function returns. +* Heap resources: Heap resources are allocated and deallocated using standard C functions like malloc and free, leaving memory allocation in the hands of the programmer. The idea here is that remaining functionality, such as allocators, are implemented as libraries. + +--- + +## Safety and Future Work + +While safety is a growing concern in programming, the current implementation has several safety challenges, similar to those in C++ or Rust's unsafe mode. + +### Future Work Includes: +1. **Library support for composable allocators**: + - Tracing or debugging allocators to detect memory leaks or other faulty behavior. +2. **Compile-time borrow checking**: + - Similar to Rust or Mojo, ensuring safer resource usage. +3. **Improved lifetime analysis**: + - Making the compiler aware of when resources (e.g., heap allocations) are introduced. + - Making the compiler aware of when resources are last used. + +--- + +## 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 we assume `struct A` is a managed type. + +To enable RAII, import the library */lib/terralibext.t* using +```terra +require "terralibext" +``` +The compiler only checks for `__init`, `__dtor` and `__copy` in case this library 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 responsible 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.t* tests `__copy`. +* *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 + ``` \ No newline at end of file From 11583afec918641091be213765787436cdeb8d68 Mon Sep 17 00:00:00 2001 From: rrhiemstar Date: Thu, 28 Nov 2024 15:09:38 +0100 Subject: [PATCH 9/9] refactored division of regular and managed assignments. --- src/terralib.lua | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/terralib.lua b/src/terralib.lua index 90914a8e3..e025eee25 100644 --- a/src/terralib.lua +++ b/src/terralib.lua @@ -2859,10 +2859,24 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return stats end + --__copy is only enabled for a set of right-hand-sides + local function validcopyrhs(from) + --allow one dereference + if from:is "operator" and #from.operands==1 then + from = from.operands[1] + end + if from:is "apply" or from:is "returnstat" or from:is "operator" then + return false + else + return true + end + end + --type check raii __copy (copy-assignment) methods. They are generated --if they are missing. local function checkraiicopyassignment(anchor, from, to) if not terralib.ext then return end + if not validcopyrhs(from) then return end --check for 'from.type.methods.__copy' and 'to.type.methods.__copy' and generate them --if needed if not (ismanaged(from, "__copy") or ismanaged(to, "__copy")) then @@ -3323,29 +3337,15 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) 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 'checkraiicopyassignment' 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" or r:is "structcast" then - if checkraiicopyassignment(anchor, r, lhs[i]) then - cpassign = true - end - end - end - if cpassign then + local to, from = lhs[i], rhs[i] + if from and checkraiicopyassignment(anchor, from, to) then --add assignment by __copy call - byfcall.lhs:insert(lhs[i]) - byfcall.rhs:insert(rhs[i]) + byfcall.rhs:insert(from) + byfcall.lhs:insert(to) else --default to regular assignment - regular.lhs:insert(lhs[i]) - regular.rhs:insert(rhs[i]) + regular.rhs:insert(from) + regular.lhs:insert(to) end end if #byfcall.lhs>0 and #byfcall.lhs+#regular.lhs>1 then