Compare commits

...

2 Commits

Author SHA1 Message Date
fef4b2a5ce standalone: Disable tests requiring return of non-primitive values 2024-01-29 12:49:50 +08:00
b3736c3e99 core: Disallow returning of non-primitive values
Non-primitive values are represented by an `alloca`-ed value in the
function body, and when the pointer is returned from the function, the
`alloca`-ed object is deallocated on the stack.

Related to #54.
2024-01-29 12:49:24 +08:00
3 changed files with 119 additions and 93 deletions

View File

@ -3,7 +3,7 @@ use crate::typecheck::typedef::TypeEnum;
use super::type_inferencer::Inferencer; use super::type_inferencer::Inferencer;
use super::typedef::Type; use super::typedef::Type;
use nac3parser::ast::{self, Constant, Expr, ExprKind, Operator::{LShift, RShift}, Stmt, StmtKind, StrRef}; use nac3parser::ast::{self, Constant, Expr, ExprKind, Operator::{LShift, RShift}, Stmt, StmtKind, StrRef};
use std::{collections::HashSet, iter::once}; use std::{collections::HashSet, iter::once, ops::Not};
impl<'a> Inferencer<'a> { impl<'a> Inferencer<'a> {
fn should_have_value(&mut self, expr: &Expr<Option<Type>>) -> Result<(), HashSet<String>> { fn should_have_value(&mut self, expr: &Expr<Option<Type>>) -> Result<(), HashSet<String>> {
@ -302,6 +302,31 @@ impl<'a> Inferencer<'a> {
if let Some(value) = value { if let Some(value) = value {
self.check_expr(value, defined_identifiers)?; self.check_expr(value, defined_identifiers)?;
self.should_have_value(value)?; self.should_have_value(value)?;
// Check that the return value is a non-`alloca` type, effectively only allowing primitive types.
// This is a workaround preventing the caller from using a variable `alloca`-ed in the body, which
// is freed when the function returns.
if let Some(ret_ty) = value.custom {
if [
self.primitives.int32,
self.primitives.int64,
self.primitives.uint32,
self.primitives.uint64,
self.primitives.float,
self.primitives.bool,
].iter().any(|allowed_ty| self.unifier.unioned(ret_ty, *allowed_ty)).not() {
// Explicitly allow ellipsis as a return value, as the type of the ellipsis is contextually
// inferred and just generates an unconditional assertion
if matches!(value.node, ExprKind::Constant { value: Constant::Ellipsis, .. }).not() {
return Err(HashSet::from([
format!(
"return value of type {} must be a primitive",
self.unifier.stringify(ret_ty),
),
]))
}
}
}
} }
Ok(true) Ok(true)
} }

View File

@ -23,8 +23,8 @@ class A:
def get_a(self) -> int32: def get_a(self) -> int32:
return self.a return self.a
def get_b(self) -> B: # def get_b(self) -> B:
return self.b # return self.b
def run() -> int32: def run() -> int32:

View File

@ -21,8 +21,8 @@ def run() -> int32:
test_uint32() test_uint32()
test_int64() test_int64()
test_uint64() test_uint64()
test_A() # test_A()
test_B() # test_B()
return 0 return 0
def test_int32(): def test_int32():
@ -173,91 +173,92 @@ def test_uint64():
a >>= uint32(b) a >>= uint32(b)
output_uint64(a) output_uint64(a)
class A: # FIXME Fix returning objects of non-primitive types; Currently this is disabled in the function checker
a: int32 # class A:
def __init__(self, a: int32): # a: int32
self.a = a # def __init__(self, a: int32):
# self.a = a
def __add__(self, other: A) -> A: #
output_int32(self.a + other.a) # def __add__(self, other: A) -> A:
return A(self.a + other.a) # output_int32(self.a + other.a)
# return A(self.a + other.a)
def __sub__(self, other: A) -> A: #
output_int32(self.a - other.a) # def __sub__(self, other: A) -> A:
return A(self.a - other.a) # output_int32(self.a - other.a)
# return A(self.a - other.a)
def test_A(): #
a = A(17) # def test_A():
b = A(3) # a = A(17)
# b = A(3)
c = a + b #
# fail due to alloca in __add__ function # c = a + b
# output_int32(c.a) # # fail due to alloca in __add__ function
# # output_int32(c.a)
a += b #
# fail due to alloca in __add__ function # a += b
# output_int32(a.a) # # fail due to alloca in __add__ function
# # output_int32(a.a)
a = A(17) #
b = A(3) # a = A(17)
d = a - b # b = A(3)
# fail due to alloca in __add__ function # d = a - b
# output_int32(c.a) # # fail due to alloca in __add__ function
# # output_int32(c.a)
a -= b #
# fail due to alloca in __add__ function # a -= b
# output_int32(a.a) # # fail due to alloca in __add__ function
# # output_int32(a.a)
a = A(17) #
b = A(3) # a = A(17)
a.__add__(b) # b = A(3)
a.__sub__(b) # a.__add__(b)
# a.__sub__(b)
#
class B: #
a: int32 # class B:
def __init__(self, a: int32): # a: int32
self.a = a # def __init__(self, a: int32):
# self.a = a
def __add__(self, other: B) -> B: #
output_int32(self.a + other.a) # def __add__(self, other: B) -> B:
return B(self.a + other.a) # output_int32(self.a + other.a)
# return B(self.a + other.a)
def __sub__(self, other: B) -> B: #
output_int32(self.a - other.a) # def __sub__(self, other: B) -> B:
return B(self.a - other.a) # output_int32(self.a - other.a)
# return B(self.a - other.a)
def __iadd__(self, other: B) -> B: #
output_int32(self.a + other.a + 24) # def __iadd__(self, other: B) -> B:
return B(self.a + other.a + 24) # output_int32(self.a + other.a + 24)
# return B(self.a + other.a + 24)
def __isub__(self, other: B) -> B: #
output_int32(self.a - other.a - 24) # def __isub__(self, other: B) -> B:
return B(self.a - other.a - 24) # output_int32(self.a - other.a - 24)
# return B(self.a - other.a - 24)
def test_B(): #
a = B(17) # def test_B():
b = B(3) # a = B(17)
# b = B(3)
c = a + b #
# fail due to alloca in __add__ function # c = a + b
# output_int32(c.a) # # fail due to alloca in __add__ function
# # output_int32(c.a)
a += b #
# fail due to alloca in __add__ function # a += b
# output_int32(a.a) # # fail due to alloca in __add__ function
# # output_int32(a.a)
a = B(17) #
b = B(3) # a = B(17)
d = a - b # b = B(3)
# fail due to alloca in __add__ function # d = a - b
# output_int32(c.a) # # fail due to alloca in __add__ function
# # output_int32(c.a)
a -= b #
# fail due to alloca in __add__ function # a -= b
# output_int32(a.a) # # fail due to alloca in __add__ function
# # output_int32(a.a)
a = B(17) #
b = B(3) # a = B(17)
a.__add__(b) # b = B(3)
a.__sub__(b) # a.__add__(b)
# a.__sub__(b)