diff --git a/.gitignore b/.gitignore index de8902a11..7edf44f3b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ /tests/objc2 /tests/stdio.exe - *.bc *.ll *.o 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 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 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..98ca2c8e2 100644 --- a/src/terralib.lua +++ b/src/terralib.lua @@ -47,7 +47,7 @@ ident = escapedident(luaexpression expression) # removed during specializati field = recfield(ident key, tree value) | listfield(tree value) - + structbody = structentry(string key, luaexpression type) | structlist(structbody* entries) @@ -62,20 +62,20 @@ cmpxchgattr = (string? syncscope, string success_ordering, string failure_orderi atomicattr = (string? syncscope, string ordering, number? alignment, boolean isvolatile) Symbol = (Type type, string displayname, number id) Label = (string displayname, number id) -tree = +tree = # trees that are introduced in parsing and are ... # removed during specialization luaexpression(function expression, boolean isexpression) # removed during typechecking | constructoru(field* records) #untyped version | selectu(tree value, ident field) #untyped version - | method(tree value,ident name,tree* arguments) + | method(tree value,ident name,tree* arguments) | statlist(tree* statements) | fornumu(param variable, tree initial, tree limit, tree? step,block body) #untyped version | defvar(param* variables, boolean hasinit, tree* initializers) | forlist(param* variables, tree iterator, block body) | functiondefu(param* parameters, boolean is_varargs, TypeOrLuaExpression? returntype, block body) - + # introduced temporarily during specialization/typing, but removed after typing | luaobject(any value) | setteru(function setter) # temporary node introduced and removed during typechecking to handle __update and __setfield @@ -117,7 +117,7 @@ tree = | constructor(tree* expressions) | returnstat(tree expression) | setter(allocvar rhs, tree setter) # handles custom assignment behavior, real rhs is first stored in 'rhs' and then the 'setter' expression uses it - + # special purpose nodes, they only occur in specific locations, but are considered trees because they can contain typed trees | ifbranch(tree condition, block body) | switchcase(tree condition, block body) @@ -139,11 +139,11 @@ labelstate = undefinedlabel(gotostat * gotos, table* positions) #undefined label definition = functiondef(string? name, functype type, allocvar* parameters, boolean is_varargs, block body, table labeldepths, globalvalue* globalsused) | functionextern(string? name, functype type) - + globalvalue = terrafunction(definition? definition) | globalvariable(tree? initializer, number addressspace, boolean extern, boolean constant) attributes(string name, Type type, table anchor) - + overloadedterrafunction = (string name, terrafunction* definitions) ]] terra.irtypes = T @@ -160,7 +160,7 @@ local tokens = setmetatable({},{__index = function(self,idx) return idx end }) terra.isverbose = 0 --set by C api -local function dbprint(level,...) +local function dbprint(level,...) if terra.isverbose >= level then print(...) end @@ -182,7 +182,7 @@ end function T.tree:is(value) return self.kind == value end - + function terra.printraw(self) local function header(t) local mt = getmetatable(t) @@ -275,7 +275,7 @@ function terra.newanchor(depth) return setmetatable(body,terra.tree) end -function terra.istree(v) +function terra.istree(v) return T.tree:isclassof(v) end @@ -316,7 +316,7 @@ function terra.newenvironment(_luaenv) __index = function(_,idx) return self._localenv[idx] or self._luaenv[idx] end; - __newindex = function() + __newindex = function() error("cannot define global variables or assign to upvalues in an escape") end; }) @@ -341,12 +341,12 @@ local function formaterror(anchor,...) errlist:insert(anchor.filename..":"..anchor.linenumber..": ") for i = 1,select("#",...) do errlist:insert(tostring(select(i,...))) end errlist:insert("\n") - if not anchor.offset then + if not anchor.offset then return errlist:concat() end - + local filename = anchor.filename - local filetext = diagcache[filename] + local filetext = diagcache[filename] if not filetext then local file = io.open(filename,"r") if file then @@ -367,7 +367,7 @@ local function formaterror(anchor,...) while finish < filetext:len() and filetext:byte(finish + 1) ~= NL do finish = finish + 1 end - local line = filetext:sub(begin,finish) + local line = filetext:sub(begin,finish) errlist:insert(line) errlist:insert("\n") for i = begin,anchor.offset do @@ -458,7 +458,7 @@ function debug.traceback(msg,level) local whatname,what = debug.getlocal(level,2) assert(anchorname == "anchor" and whatname == "what") lines:insert("\n\t") - lines:insert(formaterror(anchor,"Errors reported during "..what):sub(1,-2)) + lines:insert(formaterror(anchor,"Errors reported during "..what):sub(1,-2)) else local short_src,currentline,linedefined = di.short_src,di.currentline,di.linedefined local file,outsideline = di.source:match("^@$terra$(.*)$terra$(%d+)$") @@ -477,7 +477,7 @@ function debug.traceback(msg,level) elseif di.what == "main" then lines:insert(" in main chunk") elseif di.what == "C" then - lines:insert( (" at %s"):format(tostring(di.func))) + lines:insert( (" at %s"):format(tostring(di.func))) else lines:insert((" in function <%s:%d>"):format(short_src,linedefined)) end @@ -555,13 +555,13 @@ function T.terrafunction:setinlined(v) assert(self:isdefined(), "attempting to set the inlining state of an undefined function") self.definition.alwaysinline = not not v assert(not (self.definition.alwaysinline and self.definition.dontoptimize), - "setinlined(true) and setoptimized(false) are incompatible") + "setinlined(true) and setoptimized(false) are incompatible") end function T.terrafunction:setoptimized(v) assert(self:isdefined(), "attempting to set the optimization state of an undefined function") self.definition.dontoptimize = not v assert(not (self.definition.alwaysinline and self.definition.dontoptimize), - "setinlined(true) and setoptimized(false) are incompatible") + "setinlined(true) and setoptimized(false) are incompatible") end function T.terrafunction:setcallingconv(v) assert(self:isdefined(), "attempting to set the calling convention of an undefined function") @@ -584,8 +584,8 @@ function T.terrafunction:printstats() end function T.terrafunction:isextern() return self.definition and self.definition.kind == "functionextern" end function T.terrafunction:isdefined() return self.definition ~= nil end -function T.terrafunction:setname(name) - self.name = tostring(name) +function T.terrafunction:setname(name) + self.name = tostring(name) if self.definition then self.definition.name = name end return self end @@ -595,19 +595,19 @@ function T.terrafunction:adddefinition(functiondef) self:resetdefinition(functiondef) end function T.terrafunction:resetdefinition(functiondef) - if T.terrafunction:isclassof(functiondef) and functiondef:isdefined() then + if T.terrafunction:isclassof(functiondef) and functiondef:isdefined() then functiondef = functiondef.definition end assert(T.definition:isclassof(functiondef), "expected a defined terra function") if self.readytocompile then error("cannot reset a definition of function that has already been compiled",2) end - if self.type ~= functiondef.type and self.type ~= terra.types.placeholderfunction then + if self.type ~= functiondef.type and self.type ~= terra.types.placeholderfunction then error(("attempting to define terra function declaration with type %s with a terra function definition of type %s"):format(tostring(self.type),tostring(functiondef.type))) end self.definition,self.type,functiondef.name = functiondef,functiondef.type,assert(self.name) end function T.terrafunction:gettype(nop) assert(nop == nil, ":gettype no longer takes any callbacks for when a function is complete") - if self.type == terra.types.placeholderfunction then + if self.type == terra.types.placeholderfunction then error("function being recursively referenced needs an explicit return type, function defintion at: "..formaterror(self.anchor,""),2) end return self.type @@ -673,7 +673,7 @@ local function constantcheck(e,checklvalue) if e.expression.type:isarray() then if checklvalue then constantcheck(e.expression,true) - else + else erroratlocation(e,"non-constant cast of array to pointer used as a constant initializer") end else constantcheck(e.expression) end @@ -684,7 +684,7 @@ local function constantcheck(e,checklvalue) else erroratlocation(e,"non-constant expression being used as a constant initializer") end - return e + return e end local function createglobalinitializer(anchor, typ, c) @@ -760,7 +760,7 @@ function terra.newtarget(tbl) return setmetatable({ llvm_target = cdatawithdestructor(terra.inittarget(Triple,CPU,Features,FloatABIHard),terra.freetarget), Triple = Triple, cnametostruct = { general = {}, tagged = {}} --map from llvm_name -> terra type used to make c structs unique per llvm_name - },terra.target) + },terra.target) end function terra.target:getorcreatecstruct(displayname,tagged) local namespace @@ -797,7 +797,7 @@ local function createoptimizationprofile(profile) end -- Ok, leave it alone. else - error("expected fastmath to be a boolean or string but found " .. type(fastmath)) + error("expected fastmath to be a boolean or string but found " .. type(fastmath)) end profile["fastmath"] = fastmath -- Write it back. @@ -810,7 +810,7 @@ compilationunit.__index = compilationunit function terra.newcompilationunit(target,opt,profile) assert(terra.istarget(target),"expected a target object") profile = createoptimizationprofile(profile) - return setmetatable({ symbols = newweakkeytable(), + return setmetatable({ symbols = newweakkeytable(), collectfunctions = opt, llvm_cu = cdatawithdestructor(terra.initcompilationunit(target.llvm_target,opt,profile),terra.freecompilationunit) },compilationunit) -- mapping from Types,Functions,Globals,Constants -> llvm value associated with them for this compilation end @@ -863,7 +863,7 @@ end function terra.createmacro(fromterra,fromlua) return setmetatable({fromterra = fromterra,fromlua = fromlua}, terra.macro) end -function terra.internalmacro(...) +function terra.internalmacro(...) local m = terra.createmacro(...) m._internal = true return m @@ -915,7 +915,7 @@ function T.quote:asvalue() elseif e:is "constructor" then local t,typ = {},e.type for i,r in ipairs(typ:getentries()) do - local v,e = getvalue(e.expressions[i]) + local v,e = getvalue(e.expressions[i]) if e then return nil,e end local key = typ.convertible == "tuple" and i or r.field t[key] = v @@ -923,7 +923,7 @@ function T.quote:asvalue() return t elseif e:is "var" then return e.symbol elseif e:is "luaobject" then - return e.value + return e.value else local runconstantprop = function() return terra.constant(self):get() @@ -963,7 +963,7 @@ function T.Symbol:__tostring() end function T.Symbol:tocname() return "__symbol"..tostring(self.id) end -_G["symbol"] = terra.newsymbol +_G["symbol"] = terra.newsymbol -- LABEL function terra.islabel(l) return T.Label:isclassof(l) end @@ -1013,7 +1013,7 @@ terra.asm = terra.internalmacro(function(diag,tree,returntype, asm, constraints, local args = List{...} return typecheck(newobject(tree, T.inlineasm,returntype:astype(), tostring(asm:asvalue()), not not volatile:asvalue(), tostring(constraints:asvalue()), args)) end) - + local evalluaexpression -- CONSTRUCTORS @@ -1031,7 +1031,7 @@ local function layoutstruct(st,tree,env) end return { field = v.key, type = resolvedtype } end - + local function getrecords(records) return records:map(function(v) if v.kind == "structlist" then @@ -1044,9 +1044,9 @@ local function layoutstruct(st,tree,env) local metatype = tree.metatype and evalluaexpression(env,tree.metatype) st.entries = getrecords(tree.records.entries) st.tree = tree --to track whether the struct has already beend defined - --we keep the tree to improve error reporting + --we keep the tree to improve error reporting st.anchor = tree --replace the anchor generated by newstruct with this struct definition - --this will cause errors on the type to be reported at the definition + --this will cause errors on the type to be reported at the definition if metatype then invokeuserfunction(tree,"invoking metatype function",false,metatype,st) end @@ -1095,7 +1095,7 @@ function terra.defineobjects(fmt,envfn,...) end return t,name:match("[^.]*$") end - + local decls = terralib.newlist() for i,c in ipairs(cmds) do --pass: declare all structs if "s" == c.c then @@ -1159,7 +1159,7 @@ function terra.defineobjects(fmt,envfn,...) end end diag:finishandabortiferrors("Errors reported during function declaration.",2) - + for i,c in ipairs(cmds) do -- pass: define structs if "s" == c.c and c.tree then layoutstruct(decls[i],c.tree,env) @@ -1206,7 +1206,7 @@ end -- TYPE -do +do --returns a function string -> string that makes names unique by appending numbers local function uniquenameset(sep) @@ -1227,7 +1227,7 @@ do local function tovalididentifier(name) return tostring(name):gsub("[^_%w]","_"):gsub("^(%d)","_%1"):gsub("^$","_") --sanitize input to be valid identifier end - + local function memoizefunction(fn) local info = debug.getinfo(fn,'u') local nparams = not info.isvararg and info.nparams @@ -1252,7 +1252,7 @@ do return v end end - + local types = {} local defaultproperties = { "name", "tree", "undefined", "incomplete", "convertible", "cachedcstring", "llvm_definingfunction" } for i,dp in ipairs(defaultproperties) do @@ -1269,9 +1269,9 @@ do end T.Type.__tostring = nil --force override to occur T.Type.__tostring = memoizefunction(function(self) - if self:isstruct() then + if self:isstruct() then if self.metamethods.__typename then - local status,r = pcall(function() + local status,r = pcall(function() return tostring(self.metamethods.__typename(self)) end) if status then return r end @@ -1295,7 +1295,7 @@ do if not self.name then error("unknown type?") end return self.name end) - + T.Type.printraw = terra.printraw function T.Type:isprimitive() return self.kind == "primitive" end function T.Type:isintegral() return self.kind == "primitive" and self.type == "integer" end @@ -1310,20 +1310,20 @@ do function T.Type:ispointertostruct() return self:ispointer() and self.type:isstruct() end function T.Type:ispointertofunction() return self:ispointer() and self.type:isfunction() end function T.Type:isaggregate() return self:isstruct() or self:isarray() end - + function T.Type:iscomplete() return not self.incomplete end - + function T.Type:isvector() return self.kind == "vector" end - + function T.Type:isunit() return types.unit == self end - + local applies_to_vectors = {"isprimitive","isintegral","isarithmetic","islogical", "canbeord"} for i,n in ipairs(applies_to_vectors) do T.Type[n.."orvector"] = function(self) - return self[n](self) or (self:isvector() and self.type[n](self.type)) + return self[n](self) or (self:isvector() and self.type[n](self.type)) end end - + --pretty print of layout of type function T.Type:layoutstring() local seen = {} @@ -1373,7 +1373,7 @@ do if not self[key] then if self[inside] then erroratlocation(self.anchor,erroronrecursion) - else + else self[inside] = true self[key] = getvalue(self) self[inside] = nil @@ -1387,25 +1387,25 @@ do local str = "struct "..nm.." { " local entries = layout.entries for i,v in ipairs(entries) do - + local prevalloc = entries[i-1] and entries[i-1].allocation local nextalloc = entries[i+1] and entries[i+1].allocation - + if v.inunion and prevalloc ~= v.allocation then str = str .. " union { " end - + local keystr = terra.islabel(v.key) and v.key:tocname() or v.key str = str..v.type:cstring().." "..keystr.."; " - + if v.inunion and nextalloc ~= v.allocation then str = str .. " }; " end - + end str = str .. "};" local status,err = pcall(ffi.cdef,str) - if not status then + if not status then if err:match("attempt to redefine") then print(("warning: attempting to define a C struct %s that has already been defined by the luajit ffi, assuming the Terra type matches it."):format(nm)) else error(err) end @@ -1463,7 +1463,7 @@ do elseif self:isstruct() then local nm = uniquecname(tostring(self)) ffi.cdef("typedef struct "..nm.." "..nm..";") --just make a typedef to the opaque type - --when the struct is + --when the struct is self.cachedcstring = nm if self.cachedlayout then definecstruct(nm,self.cachedlayout) @@ -1482,7 +1482,7 @@ do local pow2 = 1 --round N to next power of 2 while pow2 < self.N do pow2 = 2*pow2 end ffi.cdef("typedef "..value.." "..nm.." __attribute__ ((vector_size("..tostring(pow2*elemSz)..")));") - self.cachedcstring = nm + self.cachedcstring = nm elseif self == types.niltype then local nilname = uniquecname("niltype") ffi.cdef("typedef void * "..nilname..";") @@ -1495,13 +1495,13 @@ do error("NYI - cstring") end if not self.cachedcstring then error("cstring not set? "..tostring(self)) end - + --create a map from this ctype to the terra type to that we can implement terra.typeof(cdata) local ctype = ffi.typeof(self.cachedcstring) types.ctypetoterra[tonumber(ctype)] = self local rctype = ffi.typeof(self.cachedcstring.."&") types.ctypetoterra[tonumber(rctype)] = self - + if self:isstruct() then local function index(obj,idx) local method = self:getmethod(idx) @@ -1516,7 +1516,7 @@ do return self.cachedcstring end - + T.struct.getentries = memoizeproperty{ name = "entries"; @@ -1533,7 +1533,7 @@ do end local function checkentry(e,results) if type(e) == "table" then - local f = e.field or e[1] + local f = e.field or e[1] local t = e.type or e[2] if terra.types.istype(t) and (type(f) == "string" or terra.islabel(f)) then results:insert { type = t, field = f} @@ -1545,7 +1545,7 @@ do return end end - erroratlocation(self.anchor,"expected either a field type pair (e.g. { field = , type = } or {,} ), or a list of valid entries representing a union") + erroratlocation(self.anchor,"expected either a field type pair (e.g. { field = , type = } or {,} ), or a list of valid entries representing a union") end local checkedentries = terra.newlist() for i,e in ipairs(entries) do checkentry(e,checkedentries) end @@ -1561,7 +1561,7 @@ do end end T.struct.getlayout = memoizeproperty { - name = "layout"; + name = "layout"; erroronrecursion = "type recursively contains itself, or using a type whose layout failed"; getvalue = function(self) local tree = self.anchor @@ -1569,7 +1569,7 @@ do local nextallocation = 0 local uniondepth = 0 local unionsize = 0 - + local layout = { entries = terra.newlist(), keytoindex = {} @@ -1581,12 +1581,12 @@ do elseif t:isarray() then ensurelayout(t.type) elseif t == types.opaque then - reportopaque(self) + reportopaque(self) end end ensurelayout(t) local entry = { type = t, key = k, allocation = nextallocation, inunion = uniondepth > 0 } - + if layout.keytoindex[entry.key] ~= nil then erroratlocation(tree,"duplicate field ",tostring(entry.key)) end @@ -1621,7 +1621,7 @@ do end end addentrylist(entries) - + dbprint(2,"Resolved Named Struct To:") dbprintraw(2,self) if self.cachedcstring then @@ -1635,7 +1635,7 @@ do self.returntype:complete() return self end - function T.Type:complete() + function T.Type:complete() if self.incomplete then if self:isarray() then self.type:complete() @@ -1647,8 +1647,8 @@ do local layout = self:getlayout() if not layout.invalid then self.incomplete = nil --static initializers run only once - --if one of the members of this struct recursively - --calls complete on this type, then it will return before the static initializer has run + --if one of the members of this struct recursively + --calls complete on this type, then it will return before the static initializer has run for i,e in ipairs(layout.entries) do e.type:complete() end @@ -1666,7 +1666,7 @@ do function T.Type:tcomplete(anchor) return invokeuserfunction(anchor,"finalizing type",false,self.complete,self) end - + local function defaultgetmethod(self,methodname) local fnlike = self.methods[methodname] if not fnlike and terra.ismacro(self.metamethods.__methodmissing) then @@ -1696,14 +1696,14 @@ do function T.struct:getfields() return self:getlayout().entries end - + function types.istype(t) return T.Type:isclassof(t) end - + --map from luajit ffi ctype objects to corresponding terra type types.ctypetoterra = {} - + local function globaltype(name, typ, min_v, max_v) typ.name = typ.name or name rawset(_G,name,typ) @@ -1711,7 +1711,7 @@ do if min_v then function typ:min() return terra.cast(self, min_v) end end if max_v then function typ:max() return terra.cast(self, max_v) end end end - + --initialize integral types local integer_sizes = {1,2,4,8} for _,size in ipairs(integer_sizes) do @@ -1732,23 +1732,23 @@ do local typ = T.primitive("integer",size,s) globaltype(name,typ,min,max) end - end - + end + globaltype("float", T.primitive("float",4,true), -math.huge, math.huge) globaltype("double",T.primitive("float",8,true), -math.huge, math.huge) globaltype("bool", T.primitive("logical",1,false)) - + types.error,T.error.name = T.error,"" T.luaobjecttype.name = "luaobjecttype" - + types.niltype = T.niltype globaltype("niltype",T.niltype) - + types.opaque,T.opaque.incomplete = T.opaque,true globaltype("opaque", T.opaque) - + types.array,types.vector,types.functype = T.array,T.vector,T.functype - + T.functype.incomplete = true function T.functype:init() if self.isvararg and #self.parameters == 0 then error("vararg functions must have at least one concrete parameter") end @@ -1757,18 +1757,18 @@ do function T.array:init() self.incomplete = true end - + function T.vector:init() if not self.type:isprimitive() and self.type ~= T.error then error("vectors must be composed of primitive types (for now...) but found type "..tostring(self.type)) end end - + types.tuple = memoizefunction(function(...) local args = terra.newlist {...} local t = types.newstruct() for i,e in ipairs(args) do - if not types.istype(e) then + if not types.istype(e) then error("expected a type but found "..type(e)) end t.entries:insert {"_"..(i-1),e} @@ -1792,7 +1792,7 @@ do function types.newstructwithanchor(displayname,anchor) assert(displayname ~= "") local name = getuniquestructname(displayname) - local tbl = T.struct(name) + local tbl = T.struct(name) tbl.entries = List() tbl.methods = {} tbl.metamethods = {} @@ -1800,7 +1800,7 @@ do tbl.incomplete = true return tbl end - + function types.funcpointer(parameters,ret,isvararg) if types.istype(parameters) then parameters = {parameters} @@ -1839,7 +1839,7 @@ end -- TYPECHECKER function evalluaexpression(env, e) if not T.luaexpression:isclassof(e) then - error("not a lua expression?") + error("not a lua expression?") end assert(type(e.expression) == "function") local fn = e.expression @@ -1847,7 +1847,7 @@ function evalluaexpression(env, e) setfenv(fn,env) local v = invokeuserfunction(e,"evaluating Lua code from Terra",false,fn) setfenv(fn,oldenv) --otherwise, we hold false reference to env, -- in the case of an error, this function will still hold a reference - -- but without a good way of doing 'finally' without messing with the error trace there is no way around this + -- but without a good way of doing 'finally' without messing with the error trace there is no way around this return v end @@ -1866,7 +1866,7 @@ function evaltype(diag,env,typ) diag:reporterror(typ,"expected a type but found ",terra.type(v)) return terra.types.error end - + function evaluateparameterlist(diag, env, paramlist, requiretypes) local result = List() for i,p in ipairs(paramlist) do @@ -1898,21 +1898,21 @@ function evaluateparameterlist(diag, env, paramlist, requiretypes) if requiretypes and not entry.type then diag:reporterror(entry,"type must be specified for parameters and uninitialized variables") end - + end return result end - + local function semanticcheck(diag,parameters,block) local symbolenv = terra.newenvironment() - + local labelstates = {} -- map from label value to labelstate object, either representing a defined or undefined label - local globalsused = List() - + local globalsused = List() + local loopdepth = 0 local function enterloop() loopdepth = loopdepth + 1 end local function leaveloop() loopdepth = loopdepth - 1 end - + local scopeposition = List() local function getscopeposition() return List { unpack(scopeposition) } end local function getscopedepth(position) @@ -2040,7 +2040,7 @@ local function semanticcheck(diag,parameters,block) end visit(parameters) visit(block) - + --check the label table for any labels that have been referenced but not defined local labeldepths = {} for k,state in pairs(labelstates) do @@ -2050,7 +2050,7 @@ local function semanticcheck(diag,parameters,block) labeldepths[k] = getscopedepth(state.position) end end - + return labeldepths, globalsused end @@ -2058,7 +2058,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local env = terra.newenvironment(luaenv or {}) local diag = terra.newdiagnostics() simultaneousdefinitions = simultaneousdefinitions or {} - + local invokeuserfunction = function(...) diag:finishandabortiferrors("Errors reported during typechecking.",2) return invokeuserfunction(...) @@ -2067,7 +2067,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) diag:finishandabortiferrors("Errors reported during typechecking.",2) return evalluaexpression(...) end - + local function checklabel(e,stringok) if e.kind == "namedident" then return e end local r = evalluaexpression(env:combinedenv(),e.expression) @@ -2133,7 +2133,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local layout = v.type:getlayout(v) local index = layout.keytoindex[field] - + if index == nil then return nil,false end @@ -2165,7 +2165,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) elseif type(v) == "cdata" then local typ = terra.typeof(v) if typ:isaggregate() then --when an aggregate is directly referenced from Terra we get its pointer - --a constant would make an entire copy of the object + --a constant would make an entire copy of the object local ptrobj = createsingle(terra.constant(terra.types.pointer(typ),v)) return insertdereference(ptrobj) end @@ -2233,7 +2233,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) end end local structvariable, var_ref = allocvar(exp,exp.type,"") - + local entries = List() if #from.entries > #to.entries or (not explicit and #from.entries ~= #to.entries) then err("structural cast invalid, source has ",#from.entries," fields but target has only ",#to.entries) @@ -2260,14 +2260,14 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return exp, true else if ((typ:isprimitive() and exp.type:isprimitive()) or - (typ:isvector() and exp.type:isvector() and typ.N == exp.type.N)) and - not typ:islogicalorvector() and not exp.type:islogicalorvector() then + (typ:isvector() and exp.type:isvector() and typ.N == exp.type.N)) and + not typ:islogicalorvector() and not exp.type:islogicalorvector() then return createcast(exp,typ), true elseif typ:ispointer() and exp.type:ispointer() and typ.type == terra.types.opaque then --implicit cast from any pointer to &opaque return createcast(exp,typ), true elseif typ:ispointer() and exp.type == terra.types.niltype then --niltype can be any pointer return createcast(exp,typ), true - elseif typ:isstruct() and typ.convertible and exp.type:isstruct() and exp.type.convertible then + elseif typ:isstruct() and typ.convertible and exp.type:isstruct() and exp.type.convertible then return structcast(false,exp,typ,speculative), true elseif typ:ispointer() and exp.type:isarray() and typ.type == exp.type.type then return createcast(exp,typ), true @@ -2295,7 +2295,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local success,result = invokeuserfunction(exp, "invoking __cast", true,__cast,exp.type,typ,quotedexp) if success then local result = asterraexpression(exp,result) - if result.type ~= typ then + if result.type ~= typ then diag:reporterror(exp,"user-defined cast returned expression with the wrong type.") end return result,true @@ -2326,22 +2326,22 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) end return createcast(exp,typ) elseif (typ:isprimitive() and exp.type:isprimitive()) - or (typ:isvector() and exp.type:isvector() and typ.N == exp.type.N) then --explicit conversions from logicals to other primitives are allowed + or (typ:isvector() and exp.type:isvector() and typ.N == exp.type.N) then --explicit conversions from logicals to other primitives are allowed return createcast(exp,typ) - elseif typ:isstruct() and exp.type:isstruct() and exp.type.convertible then + elseif typ:isstruct() and exp.type:isstruct() and exp.type.convertible then return structcast(true,exp,typ) else return insertcast(exp,typ) --otherwise, allow any implicit casts end end function insertrecievercast(exp,typ,speculative) --casts allow for method recievers a:b(c,d) ==> b(a,c,d), but 'a' has additional allowed implicit casting rules - --type can also be == "vararg" if the expected type of the reciever was an argument to the varargs of a function (this often happens when it is a lua function) - if typ == "vararg" then - return insertaddressof(exp), true - elseif typ:ispointer() and not exp.type:ispointer() then - --implicit address of allowed for recievers - return insertcast(insertaddressof(exp),typ,speculative) - else + --type can also be == "vararg" if the expected type of the reciever was an argument to the varargs of a function (this often happens when it is a lua function) + if typ == "vararg" then + return insertaddressof(exp), true + elseif typ:ispointer() and not exp.type:ispointer() then + --implicit address of allowed for recievers + return insertcast(insertaddressof(exp),typ,speculative) + else return insertcast(exp,typ,speculative) end --notes: @@ -2407,7 +2407,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) entries:insert(rt) end return terra.types.tuple(unpack(entries)) - else + else err() return terra.types.error end @@ -2425,7 +2425,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return e:aserror() end return ee:copy { operands = List{e} }:withtype(e.type) - end + end local function meetbinary(e,property,lhs,rhs) @@ -2452,9 +2452,9 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) if #operands == 1 then return checkunary(e,operands,"isarithmeticorvector") end - + local l,r = unpack(operands) - + local function pointerlike(t) return t:ispointer() or t:isarray() end @@ -2505,15 +2505,15 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) else typ = a.type end - + a = insertcast(a,typ) b = insertcast(b,typ) - + else diag:reporterror(ee,"arguments to shift must be integers but found ",a.type," and ", b.type) end end - + return ee:copy { operands = List{a,b} }:withtype(typ) end @@ -2527,7 +2527,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) diag:reporterror(ee,"conditional in select is not the same shape as ",cond.type) end elseif cond.type ~= terra.types.bool then - diag:reporterror(ee,"expected a boolean or vector of booleans but found ",cond.type) + diag:reporterror(ee,"expected a boolean or vector of booleans but found ",cond.type) end end return ee:copy {operands = List {cond,l,r}}:withtype(t) @@ -2556,7 +2556,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local function checkoperator(ee) local op_string = ee.operator - + --check non-overloadable operators first if op_string == "@" then local e = checkexp(ee.operands[1]) @@ -2566,16 +2566,16 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local ty = terra.types.pointer(e.type) return ee:copy { operands = List {e} }:withtype(ty) end - + local op, genericoverloadmethod, unaryoverloadmethod = unpack(operator_table[op_string] or {}) - + if op == nil then diag:reporterror(ee,"operator ",op_string," not defined in terra code.") return ee:aserror() end - + local operands = ee.operands:map(checkexp) - + local overloads = terra.newlist() for i,e in ipairs(operands) do if e.type:isstruct() then @@ -2586,7 +2586,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) end end end - + if #overloads > 0 then return checkcall(ee, overloads, operands, "all", true, "expression") end @@ -2595,7 +2595,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) --functions to handle typecheck invocations (functions,methods,macros,operator overloads) local function removeluaobject(e) - if not e:is "luaobject" or e.type == terra.types.error then + if not e:is "luaobject" or e.type == terra.types.error then return e --don't repeat error messages else if terra.types.istype(e.value) then @@ -2646,7 +2646,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local function tryinsertcasts(anchor, typelists,castbehavior, speculate, allowambiguous, paramlist) local PERFECT_MATCH,CAST_MATCH,TOP = 1,2,math.huge - + local function trylist(typelist, speculate) if #typelist ~= #paramlist then if not speculate then @@ -2726,7 +2726,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) if #results > 1 and not allowambiguous then local strings = results:map(function(x) return mkstring(typelists[x.idx],"type list (",",",") ") end) diag:reporterror(anchor,"call to overloaded function is ambiguous. can apply to ",unpack(strings)) - end + end return results[1].expressions, results[1].idx end end @@ -2767,6 +2767,156 @@ 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 function tryaddmissingcopy(tp) + if tp and tp:isstruct() then + if not tp.methods.__copy then + terralib.ext.addmissing.__copy(tp) + end + end + end + --check for __copy and generate it if need be or exit early + local ftype, ttype = from.type, to.type + if (ftype and ftype:isstruct()) or (ttype and ttype:isstruct()) then + tryaddmissingcopy(ftype) + tryaddmissingcopy(ttype) + if ftype == ttype then + --equal types - exit early + 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)) @@ -2785,11 +2935,11 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) if location == "lexpression" and typ.metamethods.__update then local function setter(rhs) arguments:insert(rhs) - return checkmethodwithreciever(exp, true, "__update", fnlike, arguments, "statement") + return checkmethodwithreciever(exp, true, "__update", fnlike, arguments, "statement") end return newobject(exp,T.setteru,setter) end - return checkmethodwithreciever(exp, true, "__apply", fnlike, arguments, location) + return checkmethodwithreciever(exp, true, "__apply", fnlike, arguments, location) end end return checkcall(exp, terra.newlist { fnlike } , arguments, "none", false, location) @@ -2797,8 +2947,8 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) function checkcall(anchor, fnlikelist, arguments, castbehavior, allowambiguous, location) --arguments are always typed trees, or a lua object assert(#fnlikelist > 0) - - --collect all the terra functions, stop collecting when we reach the first + + --collect all the terra functions, stop collecting when we reach the first --macro and record it as themacro local terrafunctions = terra.newlist() local themacro = nil @@ -2832,14 +2982,14 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) if fn.type ~= terra.types.error then diag:reporterror(anchor,"expected a function but found ",fn.type) end - end + end end local function createcall(callee, paramlist) callee.type.type:tcompletefunction(anchor) return newobject(anchor,T.apply,callee,paramlist):withtype(callee.type.type.returntype) end - + if #terrafunctions > 0 then local paramlist = arguments:map(removeluaobject) local function getparametertypes(fn) --get the expected types for parameters to the call (this extends the function type to the length of the parameters if the function is vararg) @@ -2905,7 +3055,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local v = checkexp(e.value,"luavalue") local f = checklabel(e.field,true) local field = f.value - + if v:is "luaobject" then -- handle A.B where A is a luatable or type --check for and handle Type.staticmethod if terra.types.istype(v.value) and v.value:isstruct() then @@ -2927,7 +3077,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return asterraexpression(e,selected,location) end end - + if v.type:ispointertostruct() then --allow 1 implicit dereference v = insertdereference(v) end @@ -2937,12 +3087,12 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) if not success then --struct has no member field, call metamethod __entrymissing local typ = v.type - + local function checkmacro(metamethod,arguments,location) local named = terra.internalmacro(function(ctx,tree,...) return typ.metamethods[metamethod]:run(ctx,tree,field,...) end) - local getter = asterraexpression(e, named, "luaobject") + local getter = asterraexpression(e, named, "luaobject") return checkcall(v, terra.newlist{ getter }, arguments, "first", false, location) end if location == "lexpression" and typ.metamethods.__setentry then @@ -2972,7 +3122,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) elseif e:is "index" then local v = checkexp(e.value) local idx = checkexp(e.index) - local typ,lvalue = terra.types.error, v.type:ispointer() or (v.type:isarray() and v.lvalue) + local typ,lvalue = terra.types.error, v.type:ispointer() or (v.type:isarray() and v.lvalue) if v.type:ispointer() or v.type:isarray() or v.type:isvector() then typ = v.type.type if not idx.type:isintegral() and idx.type ~= terra.types.error then @@ -2993,7 +3143,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) elseif e:is "vectorconstructor" or e:is "arrayconstructor" then local entries = checkexpressions(e.expressions) local N = #entries - + local typ if e.oftype ~= nil then typ = e.oftype:tcomplete(e) @@ -3002,14 +3152,14 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) diag:reporterror(e,"cannot determine type of empty aggregate") return e:aserror() end - + --figure out what type this vector has typ = entries[1].type for i,e2 in ipairs(entries) do typ = typemeet(e,typ,e2.type) end end - + local aggtype if e:is "vectorconstructor" then if not typ:isprimitive() and typ ~= terra.types.error then @@ -3020,7 +3170,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) else aggtype = terra.types.array(typ,N) end - + --insert the casts to the right type in the parameter list local typs = entries:map(function(x) return typ end) entries = insertcasts(e,typs,entries) @@ -3049,8 +3199,8 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return e:aserror() end if not (addr.type.type:isintegral() or addr.type.type:ispointer()) then - diag:reporterror(e,"for cmpxchg address must be a pointer to an integral or pointer type, but found ", addr.type.type) - return e:aserror() + diag:reporterror(e,"for cmpxchg address must be a pointer to an integral or pointer type, but found ", addr.type.type) + return e:aserror() end local cmp = insertcast(checkexp(e.cmp),addr.type.type) local new = insertcast(checkexp(e.new),addr.type.type) @@ -3062,20 +3212,20 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return e:aserror() end if e.operator == "xchg" then - if not (addr.type.type:isintegral() or addr.type.type:isfloat()) then - diag:reporterror(e,"for operator " .. e.operator .. " address must be a pointer to an integral or floating point type, but found ", addr.type.type) - return e:aserror() - end + if not (addr.type.type:isintegral() or addr.type.type:isfloat()) then + diag:reporterror(e,"for operator " .. e.operator .. " address must be a pointer to an integral or floating point type, but found ", addr.type.type) + return e:aserror() + end elseif e.operator == "fadd" or e.operator == "fsub" or e.operator == "fmax" or e.operator == "fmin" then - if not addr.type.type:isfloat() then - diag:reporterror(e,"for operator " .. e.operator .. " address must be a pointer to a floating point type, but found ", addr.type.type) - return e:aserror() - end + if not addr.type.type:isfloat() then + diag:reporterror(e,"for operator " .. e.operator .. " address must be a pointer to a floating point type, but found ", addr.type.type) + return e:aserror() + end else - if not (addr.type.type:isintegral() or addr.type.type:ispointer()) then - diag:reporterror(e,"for operator " .. e.operator .. " address must be a pointer to an integral or pointer type, but found ", addr.type.type) - return e:aserror() - end + if not (addr.type.type:isintegral() or addr.type.type:ispointer()) then + diag:reporterror(e,"for operator " .. e.operator .. " address must be a pointer to an integral or pointer type, but found ", addr.type.type) + return e:aserror() + end end local value = insertcast(checkexp(e.value),addr.type.type) return e:copy { address = addr, value = value }:withtype(addr.type.type) @@ -3087,7 +3237,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local ns = checkstmts(e.statements) local ne = checkexpressions(e.expressions) return createlet(e,ns,ne,e.hasstatements) - elseif e:is "constructoru" then + elseif e:is "constructoru" then local paramlist = terra.newlist() local named = 0 for i,f in ipairs(e.records) do @@ -3121,7 +3271,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return e:aserror() end end - + local result = docheck(e_) --freeze all types returned by the expression (or list of expressions) if not result:is "luaobject" and not result:is "setteru" then @@ -3133,7 +3283,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) if location ~= "luavalue" then result = removeluaobject(result) end - + return result end @@ -3193,28 +3343,87 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return newobject(anchor,T.letin, stmts, List {}, true):withtype(terra.types.unit) end - local function createassignment(anchor,lhs,rhs) - 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)))) + --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" or r:is "structcast" then + if checkmetacopyassignment(anchor, r, lhs[i]) then + cpassign = true end end - newlhs[#rhs] = av - local a1,a2 = createassignment(anchor,newlhs,rhs), createassignment(anchor,lhsp,rhsp) - return createstatementlist(anchor, List {a1, a2}) + 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] + 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,"") + 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 + 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 + return trystructpatternmatching(anchor, lhs, rhs) end end + --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) + 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 @@ -3229,9 +3438,102 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return newobject(anchor,T.assignment,lhs,rhs) end + 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 + --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 + --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 + 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 @@ -3242,7 +3544,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return checkblock(s) elseif s:is "returnstat" then return s:copy { expression = checkexp(s.expression)} - elseif s:is "label" or s:is "gotostat" then + elseif s:is "label" or s:is "gotostat" then local ss = checklabel(s.label) return copyobject(s, { label = ss }) elseif s:is "breakstat" then @@ -3251,7 +3553,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return checkcondbranch(s) elseif s:is "fornumu" then local initial, limit, step = checkexp(s.initial), checkexp(s.limit), s.step and checkexp(s.step) - local t = typemeet(initial,initial.type,limit.type) + local t = typemeet(initial,initial.type,limit.type) t = step and typemeet(limit,t,step.type) or t local variables = checkformalparameterlist(List {s.variable },false) if #variables ~= 1 then @@ -3266,7 +3568,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return newobject(s,T.fornum,variable,initial,limit,step,body) elseif s:is "forlist" then local iterator = checkexp(s.iterator) - + local typ = iterator.type if typ:ispointertostruct() then typ,iterator = typ.type, insertdereference(iterator) @@ -3276,7 +3578,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) return s end local generator = typ.metamethods.__for - + local function bodycallback(...) local exps = List() for i = 1,select("#",...) do @@ -3291,7 +3593,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local stats = createstatementlist(s, List { assign, body }) return terra.newquote(stats) end - + local value = invokeuserfunction(s, "invoking __for", false ,generator,terra.newquote(iterator), bodycallback) return asterraexpression(s,value,"statement") elseif s:is "ifstat" then @@ -3314,9 +3616,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") @@ -3344,7 +3653,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) else newstats:insert(s) end - end + end for _,s in ipairs(stmts) do local r = checksingle(s) if r.kind == "statlist" then -- lists of statements are spliced directly into the list @@ -3431,7 +3740,7 @@ function typecheck(topexp,luaenv,simultaneousdefinitions) local typed_parameters = checkformalparameterlist(topexp.parameters, true) local parameter_types = typed_parameters:map("type") local body,returntype = checkreturns(checkblock(topexp.body),topexp.returntype) - + local fntype = terra.types.functype(parameter_types,returntype,topexp.is_varargs):tcompletefunction(topexp) diag:finishandabortiferrors("Errors reported during typechecking.",2) local labeldepths,globalsused = semanticcheck(diag,typed_parameters,body) @@ -3473,7 +3782,7 @@ function terra.registerinternalizedfiles(names,contents,sizes) cur.children = cur.children or {} cur.kind = "directory" if not cur.children[segment] then - cur.children[segment] = {} + cur.children[segment] = {} end cur = cur.children[segment] end @@ -3509,18 +3818,18 @@ function terra.includecstring(code,cargs,target) args:insert(clangresourcedirectory.."/include") end for _,path in ipairs(terra.systemincludes) do - args:insert("-internal-isystem") - args:insert(path) + args:insert("-internal-isystem") + args:insert(path) end -- Obey the SDKROOT variable on macOS to match Clang behavior. local sdkroot = os.getenv("SDKROOT") if sdkroot then - args:insert("-isysroot") - args:insert(sdkroot) + args:insert("-isysroot") + args:insert(sdkroot) end -- Set GNU C version to match value set by Clang: https://github.com/llvm/llvm-project/blob/f77c948d56b09b839262e258af5c6ad701e5b168/clang/lib/Driver/ToolChains/Clang.cpp#L5750-L5753 if ffi.os ~= "Windows" and terralib.llvm_version >= 100 then - args:insert("-fgnuc-version=4.2.1") + args:insert("-fgnuc-version=4.2.1") end if cargs then @@ -3554,26 +3863,26 @@ end -- GLOBAL MACROS terra.sizeof = terra.internalmacro( -function(diag,tree,typ) - return typecheck(newobject(tree,T.sizeof,typ:astype())) -end, -function (terratype,...) - terratype:complete() - return terra.llvmsizeof(terra.jitcompilationunit,terratype) -end + function(diag,tree,typ) + return typecheck(newobject(tree,T.sizeof,typ:astype())) + end, + function (terratype,...) + terratype:complete() + return terra.llvmsizeof(terra.jitcompilationunit,terratype) + end ) _G["sizeof"] = terra.sizeof _G["vector"] = terra.internalmacro( -function(diag,tree,...) - if not diag then - error("nil first argument in vector constructor") - end - if not tree then - error("nil second argument in vector constructor") - end - return typecheck(newobject(tree,T.vectorconstructor,nil,List{...})) -end, -terra.types.vector + function(diag,tree,...) + if not diag then + error("nil first argument in vector constructor") + end + if not tree then + error("nil second argument in vector constructor") + end + return typecheck(newobject(tree,T.vectorconstructor,nil,List{...})) + end, + terra.types.vector ) _G["vectorof"] = terra.internalmacro(function(diag,tree,typ,...) return typecheck(newobject(tree,T.vectorconstructor,typ:astype(),List{...})) @@ -3596,7 +3905,7 @@ local function createunpacks(tupleonly) local entries = typ:getentries() from = from and tonumber(from:asvalue()) or 1 to = to and tonumber(to:asvalue()) or #entries - for i = from,to do + for i = from,to do local e= entries[i] if e.field then local ident = newobject(tree,type(e.field) == "string" and T.namedident or T.labelident,e.field) @@ -3607,8 +3916,8 @@ local function createunpacks(tupleonly) end local function unpacklua(cdata,from,to) local t = type(cdata) == "cdata" and terra.typeof(cdata) - if not t or not t:isstruct() or (tupleonly and t.convertible ~= "tuple") then - return cdata + if not t or not t:isstruct() or (tupleonly and t.convertible ~= "tuple") then + return cdata end local results = terralib.newlist() local entries = t:getentries() @@ -3647,9 +3956,9 @@ local function createattributetable(q) if attr.align ~= nil and type(attr.align) ~= "number" then error("align attribute must be a number, not a " .. type(attr.align)) end - return T.attr(attr.nontemporal and true or false, - attr.align or nil, - attr.isvolatile and true or false) + return T.attr(attr.nontemporal and true or false, + attr.align or nil, + attr.isvolatile and true or false) end terra.attrload = terra.internalmacro( function(diag,tree,addr,attr) @@ -3681,8 +3990,8 @@ local function createfenceattributetable(q) error("ordering attribute must be a string, not a " .. type(attr.ordering)) end return T.fenceattr( - attr.syncscope or nil, - attr.ordering or nil) + attr.syncscope or nil, + attr.ordering or nil) end terra.fence = terra.internalmacro( function(diag,tree,attr) @@ -3716,12 +4025,12 @@ local function createcmpxchgattributetable(q) error("align attribute must be a number, not a " .. type(attr.align)) end return T.cmpxchgattr( - attr.syncscope or nil, - attr.success_ordering or nil, - attr.failure_ordering or nil, - attr.align or nil, - attr.isvolatile and true or false, - attr.isweak and true or false) + attr.syncscope or nil, + attr.success_ordering or nil, + attr.failure_ordering or nil, + attr.align or nil, + attr.isvolatile and true or false, + attr.isweak and true or false) end terra.cmpxchg = terra.internalmacro( function(diag,tree,addr,cmp,new,attr) @@ -3749,10 +4058,10 @@ local function createatomicattributetable(q) error("align attribute must be a number, not a " .. type(attr.align)) end return T.atomicattr( - attr.syncscope or nil, - attr.ordering or nil, - attr.align or nil, - attr.isvolatile and true or false) + attr.syncscope or nil, + attr.ordering or nil, + attr.align or nil, + attr.isvolatile and true or false) end terra.atomicrmw = terra.internalmacro( function(diag,tree,op,addr,value,attr) @@ -3761,7 +4070,7 @@ terra.atomicrmw = terra.internalmacro( function(diag,tree,op,addr,value,attr) end local op_value = op:asvalue() if type(op_value) ~= "string" then - error("operator argument to atomicrmw must be a string, not a " .. type(op_value)) + error("operator argument to atomicrmw must be a string, not a " .. type(op_value)) end return typecheck(newobject(tree,T.atomicrmw,op_value,addr,value,createatomicattributetable(attr))) end) @@ -3775,7 +4084,7 @@ function prettystring(toptree,breaklines) local buffer = terralib.newlist() -- list of strings that concat together into the pretty output local env = terra.newenvironment({}) local indentstack = terralib.newlist{ 0 } -- the depth of each indent level - + local currentlinelength = 0 local function enterblock() indentstack:insert(indentstack[#indentstack] + 4) @@ -3789,7 +4098,7 @@ function prettystring(toptree,breaklines) local function emit(fmt,...) local function toformat(x) if type(x) ~= "number" and type(x) ~= "string" then - return tostring(x) + return tostring(x) else return x end @@ -3805,11 +4114,11 @@ function prettystring(toptree,breaklines) end local function differentlocation(a,b) return (a.linenumber ~= b.linenumber or a.filename ~= b.filename) - end + end local lastanchor = { linenumber = "", filename = "" } local function begin(anchor,...) local fname = differentlocation(lastanchor,anchor) and (anchor.filename..":"..anchor.linenumber..": ") - or "" + or "" emit("%s",pad(fname,24)) currentlinelength = 0 emit((" "):rep(indentstack[#indentstack])) @@ -3857,10 +4166,10 @@ function prettystring(toptree,breaklines) end local function emitParam(p) assert(T.allocvar:isclassof(p) or T.param:isclassof(p)) - if T.unevaluatedparam:isclassof(p) then + if T.unevaluatedparam:isclassof(p) then emit("%s%s",IdentToString(p.name),p.type and " : "..luaexpression or "") else - emitIdent(p.name,p.symbol) + emitIdent(p.name,p.symbol) if p.type then emit(" : %s",p.type) end end end @@ -3924,7 +4233,7 @@ function prettystring(toptree,breaklines) begin(s,"for ") emitParam(s.variable) emit(" = ") - emitExp(s.initial) emit(",") emitExp(s.limit) + emitExp(s.initial) emit(",") emitExp(s.limit) if s.step then emit(",") emitExp(s.step) end emit(" do\n") emitStmt(s.body) @@ -3994,7 +4303,7 @@ function prettystring(toptree,breaklines) emit("\n") end end - + local function makeprectable(...) local lst = {...} local sz = #lst @@ -4006,13 +4315,13 @@ function prettystring(toptree,breaklines) end local prectable = makeprectable( - "+",7,"-",7,"*",8,"/",8,"%",8, - "^",11,"..",6,"<<",4,">>",4, - "==",3,"<",3,"<=",3, - "~=",3,">",3,">=",3, - "and",2,"or",1, - "@",9,"&",9,"not",9,"select",12) - + "+",7,"-",7,"*",8,"/",8,"%",8, + "^",11,"..",6,"<<",4,">>",4, + "==",3,"<",3,"<=",3, + "~=",3,">",3,">=",3, + "and",2,"or",1, + "@",9,"&",9,"not",9,"select",12) + local function getprec(e) if e:is "operator" then if "-" == e.operator and #e.operands == 1 then return 9 --unary minus case @@ -4111,7 +4420,7 @@ function prettystring(toptree,breaklines) emit(")") elseif e:is "constructor" then local success,keys = pcall(function() return e.type:getlayout().entries:map(function(e) return tostring(e.key) end) end) - if not success then emit(" = ") + if not success then emit(" = ") else emitList(keys,"",", "," = ",emit) end emitParamList(e.expressions) elseif e:is "constructoru" then @@ -4181,11 +4490,11 @@ function prettystring(toptree,breaklines) emit("",tostring(e.value)) end elseif e:is "method" then - doparens(e,e.value) - emit(":%s",IdentToString(e.name)) - emit("(") - emitParamList(e.arguments) - emit(")") + doparens(e,e.value) + emit(":%s",IdentToString(e.name)) + emit("(") + emitParamList(e.arguments) + emit(")") elseif e:is "debuginfo" then emit("debuginfo(%q,%d)",e.customfilename,e.customlinenumber) elseif e:is "inlineasm" then @@ -4305,12 +4614,12 @@ function terra.saveobj(filename,filekind,env,arguments,target,optimize) end local profile if optimize == nil or type(optimize) == "boolean" then - profile = {} + profile = {} elseif type(optimize) == "table" then - profile = optimize - optimize = optimize["optimize"] + profile = optimize + optimize = optimize["optimize"] else - error("expected optimize to be a boolean or table but found " .. type(optimize)) + error("expected optimize to be a boolean or table but found " .. type(optimize)) end local cu = terra.newcompilationunit(target or terra.nativetarget,false,profile) @@ -4326,27 +4635,27 @@ end -- configure path variables if ffi.os == "Windows" then - terra.cudahome = os.getenv("CUDA_PATH") + terra.cudahome = os.getenv("CUDA_PATH") else - terra.cudahome = os.getenv("CUDA_HOME") or "/usr/local/cuda" + terra.cudahome = os.getenv("CUDA_HOME") or "/usr/local/cuda" end -terra.cudalibpaths = ({ OSX = {driver = "/usr/local/cuda/lib/libcuda.dylib", runtime = "$CUDA_HOME/lib/libcudart.dylib", nvvm = "$CUDA_HOME/nvvm/lib/libnvvm.dylib"}; - Linux = {driver = "libcuda.so", runtime = "$CUDA_HOME/lib64/libcudart.so", nvvm = "$CUDA_HOME/nvvm/lib64/libnvvm.so"}; - Windows = {driver = "nvcuda.dll", runtime = "$CUDA_HOME\\bin\\cudart64_*.dll", nvvm = "$CUDA_HOME\\nvvm\\bin\\nvvm64_*.dll"}; })[ffi.os] +terra.cudalibpaths = ({ OSX = {driver = "/usr/local/cuda/lib/libcuda.dylib", runtime = "$CUDA_HOME/lib/libcudart.dylib", nvvm = "$CUDA_HOME/nvvm/lib/libnvvm.dylib"}; + Linux = {driver = "libcuda.so", runtime = "$CUDA_HOME/lib64/libcudart.so", nvvm = "$CUDA_HOME/nvvm/lib64/libnvvm.so"}; + Windows = {driver = "nvcuda.dll", runtime = "$CUDA_HOME\\bin\\cudart64_*.dll", nvvm = "$CUDA_HOME\\nvvm\\bin\\nvvm64_*.dll"}; })[ffi.os] -- OS's that are not supported by CUDA will have an undefined value here if terra.cudalibpaths and terra.cudahome then - for name,path in pairs(terra.cudalibpaths) do - path = path:gsub("%$CUDA_HOME",terra.cudahome) - if path:match("%*") and ffi.os == "Windows" then - local F = io.popen(('dir /b /s "%s" 2> nul'):format(path)) - if F then - path = F:read("*line") or path - F:close() - end - end - terra.cudalibpaths[name] = path - end -end + for name,path in pairs(terra.cudalibpaths) do + path = path:gsub("%$CUDA_HOME",terra.cudahome) + if path:match("%*") and ffi.os == "Windows" then + local F = io.popen(('dir /b /s "%s" 2> nul'):format(path)) + if F then + path = F:read("*line") or path + F:close() + end + end + terra.cudalibpaths[name] = path + end +end local cudatarget function terra.getcudatarget() @@ -4368,108 +4677,108 @@ if ffi.os == "Windows" then terra.vclinker = ([[%sbin\Host%s\%s\link.exe]]):format(terra.vshome, os.getenv("VSCMD_ARG_HOST_ARCH"), os.getenv("VSCMD_ARG_TGT_ARCH")) end terra.includepath = os.getenv("INCLUDE") - + function terra.getvclinker(target) - local vclib = os.getenv("LIB") - local vcpath = terra.vcpath or os.getenv("Path") - vclib,vcpath = "LIB="..vclib,"Path="..vcpath - return terra.vclinker,vclib,vcpath + local vclib = os.getenv("LIB") + local vcpath = terra.vcpath or os.getenv("Path") + vclib,vcpath = "LIB="..vclib,"Path="..vcpath + return terra.vclinker,vclib,vcpath end else local function compareversion(form, a, b) - if (a == nil) or (b == nil) then return true end - - local alist = {} - for e in string.gmatch(a, form) do table.insert(alist, tonumber(e)) end - local blist = {} - for e in string.gmatch(b, form) do table.insert(blist, tonumber(e)) end - - for i=1,#alist do - if alist[i] ~= blist[i] then - return alist[i] > blist[i] - end - end - return false - end - + if (a == nil) or (b == nil) then return true end + + local alist = {} + for e in string.gmatch(a, form) do table.insert(alist, tonumber(e)) end + local blist = {} + for e in string.gmatch(b, form) do table.insert(blist, tonumber(e)) end + + for i=1,#alist do + if alist[i] ~= blist[i] then + return alist[i] > blist[i] + end + end + return false + end + -- First find the latest Windows SDK installed using the registry local installedroots = [[SOFTWARE\Microsoft\Windows Kits\Installed Roots]] local windowsdk = terra.queryregvalue(installedroots, "KitsRoot10") if windowsdk == nil then - windowsdk = terra.queryregvalue(installedroots, "KitsRoot81") - if windowsdk == nil then - error "Can't find windows SDK version 8.1 or 10! Try running Terra in a Native Tools Developer Console instead." - end - - local version = nil - for i, v in ipairs(terra.listsubdirectories(windowsdk .. "lib")) do - if compareversion("%d+", v, version) then - version = v - end - end - if version == nil then - error "Can't find valid version subdirectory in the SDK! Is the Windows 8.1 SDK installation corrupt?" - end - - terra.sdklib = windowsdk .. "lib\\" .. version + windowsdk = terra.queryregvalue(installedroots, "KitsRoot81") + if windowsdk == nil then + error "Can't find windows SDK version 8.1 or 10! Try running Terra in a Native Tools Developer Console instead." + end + + local version = nil + for i, v in ipairs(terra.listsubdirectories(windowsdk .. "lib")) do + if compareversion("%d+", v, version) then + version = v + end + end + if version == nil then + error "Can't find valid version subdirectory in the SDK! Is the Windows 8.1 SDK installation corrupt?" + end + + terra.sdklib = windowsdk .. "lib\\" .. version else - -- Find highest version. For sanity reasons, we assume the same version folders are in both lib/ and include/ - local version = nil - for i, v in ipairs(terra.listsubdirectories(windowsdk .. "include")) do - if compareversion("%d+", v, version) then - version = v - end - end - if version == nil then - error "Can't find valid version subdirectory in the SDK! Is the SDK installation corrupt?" - end - - terra.sdklib = windowsdk .. "lib\\" .. version - end - + -- Find highest version. For sanity reasons, we assume the same version folders are in both lib/ and include/ + local version = nil + for i, v in ipairs(terra.listsubdirectories(windowsdk .. "include")) do + if compareversion("%d+", v, version) then + version = v + end + end + if version == nil then + error "Can't find valid version subdirectory in the SDK! Is the SDK installation corrupt?" + end + + terra.sdklib = windowsdk .. "lib\\" .. version + end + terra.vshome = terra.findvisualstudiotoolchain() - if terra.vshome == nil then - terra.vshome = terra.queryregvalue([[SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0]], "ShellFolder") or - terra.queryregvalue([[SOFTWARE\WOW6432Node\Microsoft\VisualStudio\12.0]], "ShellFolder") or - terra.queryregvalue([[SOFTWARE\WOW6432Node\Microsoft\VisualStudio\11.0]], "ShellFolder") or - terra.queryregvalue([[SOFTWARE\WOW6432Node\Microsoft\VisualStudio\10.0]], "ShellFolder") - - if terra.vshome == nil then - error "Can't find Visual Studio either via COM or the registry! Try running Terra in a Native Tools Developer Console instead." - end - terra.vshome = terra.vshome .. "VC\\" - terra.vsarch64 = "amd64" -- Before 2017, Visual Studio had it's own special architecture convention, because who needs standards - terra.vslinkpath = function(host, target) - if string.lower(host) == string.lower(target) then - return ([[BIN\%s\]]):format(host) - else - return ([[BIN\%s_%s\]]):format(host, target) + if terra.vshome == nil then + terra.vshome = terra.queryregvalue([[SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0]], "ShellFolder") or + terra.queryregvalue([[SOFTWARE\WOW6432Node\Microsoft\VisualStudio\12.0]], "ShellFolder") or + terra.queryregvalue([[SOFTWARE\WOW6432Node\Microsoft\VisualStudio\11.0]], "ShellFolder") or + terra.queryregvalue([[SOFTWARE\WOW6432Node\Microsoft\VisualStudio\10.0]], "ShellFolder") + + if terra.vshome == nil then + error "Can't find Visual Studio either via COM or the registry! Try running Terra in a Native Tools Developer Console instead." + end + terra.vshome = terra.vshome .. "VC\\" + terra.vsarch64 = "amd64" -- Before 2017, Visual Studio had it's own special architecture convention, because who needs standards + terra.vslinkpath = function(host, target) + if string.lower(host) == string.lower(target) then + return ([[BIN\%s\]]):format(host) + else + return ([[BIN\%s_%s\]]):format(host, target) + end end - end else - if terra.vshome[#terra.vshome] ~= '\\' then - terra.vshome = terra.vshome .. "\\" - end - terra.vsarch64 = "x64" - terra.vslinkpath = function(host, target) return ([[bin\Host%s\%s\]]):format(host, target) end + if terra.vshome[#terra.vshome] ~= '\\' then + terra.vshome = terra.vshome .. "\\" + end + terra.vsarch64 = "x64" + terra.vslinkpath = function(host, target) return ([[bin\Host%s\%s\]]):format(host, target) end end - + function terra.getvclinker(target) --get the linker, and guess the needed environment variables for Windows if they are not set ... target = target or "x86_64" local winarch = target if target == "x86_64" then - target = terra.vsarch64 - winarch = "x64" -- Unbelievably, Visual Studio didn't follow the Windows SDK convention until 2017+ + target = terra.vsarch64 + winarch = "x64" -- Unbelievably, Visual Studio didn't follow the Windows SDK convention until 2017+ elseif target == "aarch64" or target == "aarch64_be" then - target = "arm64" - winarch = "arm64" + target = "arm64" + winarch = "arm64" end - + local host = ffi.arch if host == "x64" then - host = terra.vsarch64 + host = terra.vsarch64 end - + local linker = terra.vshome..terra.vslinkpath(host, target).."link.exe" local vclib = ([[%s\um\%s;%s\ucrt\%s;]]):format(terra.sdklib, winarch, terra.sdklib, winarch) .. ([[%sLIB\%s;%sATLMFC\LIB\%s;]]):format(terra.vshome, target, terra.vshome, target) local vcpath = terra.vcpath or (os.getenv("Path") or "")..";"..terra.vshome..[[BIN;]]..terra.vshome..terra.vslinkpath(host, host)..";" -- deals with VS2017 cross-compile nonsense: https://github.com/rust-lang/rust/issues/31063 @@ -4487,7 +4796,7 @@ end local defaultterrahome = ffi.os == "Windows" and "C:\\Program Files\\terra" or "/usr/local" terra.terrahome = os.getenv("TERRA_HOME") or terra.terrahome or defaultterrahome local terradefaultpath = ffi.os == "Windows" and ";.\\?.t;"..terra.terrahome.."\\include\\?.t;" - or ";./?.t;"..terra.terrahome.."/share/terra/?.t;" + or ";./?.t;"..terra.terrahome.."/share/terra/?.t;" package.terrapath = (os.getenv("TERRA_PATH") or ";;"):gsub(";;",terradefaultpath) @@ -4638,7 +4947,7 @@ function terra.linkllvmstring(str,target) return terra.linkllvm(str,target,true) terra.languageextension = { tokentype = {}; --metatable for tokentype objects - tokenkindtotoken = {}; --map from token's kind id (terra.kind.name), to the singleton table (terra.languageextension.name) + tokenkindtotoken = {}; --map from token's kind id (terra.kind.name), to the singleton table (terra.languageextension.name) } function terra.importlanguage(languages,entrypoints,langstring) @@ -4647,7 +4956,7 @@ function terra.importlanguage(languages,entrypoints,langstring) if not lang or type(lang) ~= "table" then error("expected a table to define language") end lang.name = lang.name or "anonymous" local function haslist(field,typ) - if not lang[field] then + if not lang[field] then error(field .. " expected to be list of "..typ) end for i,k in ipairs(lang[field]) do @@ -4658,7 +4967,7 @@ function terra.importlanguage(languages,entrypoints,langstring) end haslist("keywords","string") haslist("entrypoints","string") - + for i,e in ipairs(lang.entrypoints) do if entrypoints[e] then error(("language '%s' uses entrypoint '%s' already defined by language '%s'"):format(lang.name,e,entrypoints[e].name),-1) @@ -4704,7 +5013,7 @@ end function terra.runlanguage(lang,cur,lookahead,next,embeddedcode,source,isstatement,islocal) local lex = {} - + lex.name = terra.languageextension.name lex.string = terra.languageextension.string lex.number = terra.languageextension.number @@ -4739,7 +5048,7 @@ function terra.runlanguage(lang,cur,lookahead,next,embeddedcode,source,isstateme return v end local function doembeddedcode(self,isterra,isexp) - self._cur,self._lookahead = nil,nil --parsing an expression invalidates our lua representations + self._cur,self._lookahead = nil,nil --parsing an expression invalidates our lua representations local expr = embeddedcode(isterra,isexp) return function(env) local oldenv = getfenv(expr) @@ -4766,7 +5075,7 @@ function terra.runlanguage(lang,cur,lookahead,next,embeddedcode,source,isstateme function lex:typetostring(name) return name end - + function lex:nextif(typ) if self:cur().type == typ then return self:next() @@ -4787,13 +5096,13 @@ function terra.runlanguage(lang,cur,lookahead,next,embeddedcode,source,isstateme end function lex:error(msg) error(msg,0) --,0 suppresses the addition of line number information, which we do not want here since - --this is a user-caused errors + --this is a user-caused errors end function lex:errorexpected(what) self:error(what.." expected") end function lex:expectmatch(typ,openingtokentype,linenumber) - local n = self:nextif(typ) + local n = self:nextif(typ) if not n then if self:cur().linenumber == linenumber then lex:errorexpected(tostring(typ)) @@ -4814,7 +5123,7 @@ function terra.runlanguage(lang,cur,lookahead,next,embeddedcode,source,isstateme else lex:error("unexpected token") end - + if not constructor or type(constructor) ~= "function" then error("expected language to return a construction function") end @@ -4824,9 +5133,9 @@ function terra.runlanguage(lang,cur,lookahead,next,embeddedcode,source,isstateme return b == 1 and e == string.len(str) end - --fixup names + --fixup names - if not names then + if not names then names = {} end @@ -4860,26 +5169,26 @@ function terra.runlanguage(lang,cur,lookahead,next,embeddedcode,source,isstateme end _G["operator"] = terra.internalmacro(function(diag,anchor,op,...) - local tbl = { - __sub = "-"; - __add = "+"; - __mul = "*"; - __div = "/"; - __mod = "%"; - __lt = "<"; - __le = "<="; - __gt = ">"; - __ge = ">="; - __eq = "=="; - __ne = "~="; - __and = "and"; - __or = "or"; - __not = "not"; - __xor = "^"; - __lshift = "<<"; - __rshift = ">>"; - __select = "select"; - } + local tbl = { + __sub = "-"; + __add = "+"; + __mul = "*"; + __div = "/"; + __mod = "%"; + __lt = "<"; + __le = "<="; + __gt = ">"; + __ge = ">="; + __eq = "=="; + __ne = "~="; + __and = "and"; + __or = "or"; + __not = "not"; + __xor = "^"; + __lshift = "<<"; + __rshift = ">>"; + __select = "select"; + } local opv = op:asvalue() opv = tbl[opv] or opv --operator can be __add or + return typecheck(newobject(anchor,T.operator,opv,List{...})) 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