Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ lazy_static! {
E139, Error, include_str!("./error_codes/E139.md"), // Linker invocation failed (spawn / cmdline)
E140, Error, include_str!("./error_codes/E140.md"), // ':=' used for an output parameter
E141, Error, include_str!("./error_codes/E141.md"), // Member access on a non-auto-deref pointer base
// E142..E149 are used on later branches, E150 matches the same warning there
E150, Warning, include_str!("./error_codes/E150.md"), // ABS on an unsigned value has no effect
);
}

Expand Down
25 changes: 25 additions & 0 deletions compiler/plc_diagnostics/src/diagnostics/error_codes/E150.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# E150 - ABS on a value of unsigned type has no effect

`ABS` returns the absolute value of its argument. A value of an unsigned type cannot be
negative, so `ABS` returns the argument unchanged. Such a call usually points at a
mistake, for example an argument expression that already underflowed:

```iecst
FUNCTION delta : UINT
VAR_INPUT
a, b : UINT;
END_VAR
// WRONG: a - b underflows for a < b, ABS does not correct the result
delta := ABS(a - b);
END_FUNCTION
```

If the distance between two unsigned values is needed, compare them first:

```iecst
IF a >= b THEN
delta := a - b;
ELSE
delta := b - a;
END_IF
```
120 changes: 120 additions & 0 deletions src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,69 @@ lazy_static! {
}
}
),
// The standard library provides no ABS implementations for unsigned types; these
// builtins provide them so the generic resolver binds e.g. ABS(UINT#1) to
// ABS__UINT instead of inventing an external declaration that nothing implements
(
"ABS__USINT",
BuiltIn {
decl: "FUNCTION ABS__USINT : USINT
VAR_INPUT
IN : USINT;
END_VAR
END_FUNCTION
",
annotation: None,
validation: Some(validate_abs_unsigned_call),
generic_name_resolver: no_generic_name_resolver,
code: generate_abs_identity,
}
),
(
"ABS__UINT",
BuiltIn {
decl: "FUNCTION ABS__UINT : UINT
VAR_INPUT
IN : UINT;
END_VAR
END_FUNCTION
",
annotation: None,
validation: Some(validate_abs_unsigned_call),
generic_name_resolver: no_generic_name_resolver,
code: generate_abs_identity,
}
),
(
"ABS__UDINT",
BuiltIn {
decl: "FUNCTION ABS__UDINT : UDINT
VAR_INPUT
IN : UDINT;
END_VAR
END_FUNCTION
",
annotation: None,
validation: Some(validate_abs_unsigned_call),
generic_name_resolver: no_generic_name_resolver,
code: generate_abs_identity,
}
),
(
"ABS__ULINT",
BuiltIn {
decl: "FUNCTION ABS__ULINT : ULINT
VAR_INPUT
IN : ULINT;
END_VAR
END_FUNCTION
",
annotation: None,
validation: Some(validate_abs_unsigned_call),
generic_name_resolver: no_generic_name_resolver,
code: generate_abs_identity,
}
),
// TODO: MOD and AND/OR/XOR/NOT ANY_BIT ( NOT also supports boolean ) - FIXME: these are all keywords and therefore conflicting
(
"GT",
Expand Down Expand Up @@ -1010,6 +1073,63 @@ fn validate_argument_count(
}
}

/// Validates a call bound to one of the unsigned ABS builtins. ABS of an unsigned value
/// returns the value unchanged, such a call is almost always a mistake (e.g. an argument
/// expression that already underflowed).
fn validate_abs_unsigned_call(
validator: &mut Validator,
operator: &AstNode,
parameters: Option<&AstNode>,
annotations: &dyn AnnotationMap,
index: &Index,
) {
validate_argument_count(validator, operator, &parameters, 1);

let Some(param) = parameters.map(flatten_expression_list).and_then(|it| it.first().copied()) else {
return;
};
let param = extract_actual_parameter(param);

// arguments whose actual type does not satisfy ANY_NUM already report E062,
// stay silent for those
if !annotations.get_type(param, index).is_some_and(|it| it.has_nature(TypeNature::Num, index)) {
return;
}

// the argument's type hint carries the concrete type derived for T
let Some(data_type) =
annotations.get_type_hint(param, index).or_else(|| annotations.get_type(param, index))
else {
return;
};

if index.get_intrinsic_type(data_type).get_type_information().is_unsigned_int() {
validator.push_diagnostic(
Diagnostic::new(format!(
"ABS on a value of unsigned type '{}' has no effect",
data_type.get_name()
))
.with_error_code("E150")
.with_location(operator),
);
}
}

/// Codegen for the unsigned ABS builtins: |x| = x for unsigned values, the argument is
/// generated as-is
fn generate_abs_identity<'ink, 'b>(
generator: &'b ExpressionCodeGenerator<'ink, 'b>,
params: &[&AstNode],
location: SourceLocation,
) -> Result<ExpressionValue<'ink>, CodegenError> {
if let [param] = params {
let param = extract_actual_parameter(param);
generator.generate_expression(param).map(ExpressionValue::RValue)
} else {
Err(Diagnostic::codegen_error("Expected exactly one parameter for ABS", location).into())
}
}

fn validate_constant_parameters(
validator: &mut Validator,
parameters: &Option<&AstNode>,
Expand Down
4 changes: 3 additions & 1 deletion src/codegen/generators/pou_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ pub fn generate_implementation_stubs<'ink>(
.filter_map(|name| index.find_implementation_by_name(name).map(|it| (name, it)))
.collect::<FxIndexMap<_, _>>();
for (name, implementation) in implementations {
if !implementation.is_generic() {
// builtins (e.g. ABS__UINT) are generated inline by their codegen hook and must
// not leave an external declaration behind
if !implementation.is_generic() && index.get_builtin_function(name).is_none() {
let curr_f = pou_generator.generate_implementation_stub(
implementation,
module,
Expand Down
54 changes: 54 additions & 0 deletions src/codegen/tests/expression_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,60 @@ fn builtin_function_call_move() {
"#);
}

#[test]
fn builtin_function_call_abs_unsigned_ints() {
// the generic declaration mimics the one the standard library includes provide;
// the unsigned monomorphs bind to the builtins and generate the argument as-is
let result = codegen(
"FUNCTION ABS<T: ANY_NUM> : T
VAR_INPUT
IN : T;
END_VAR
END_FUNCTION

PROGRAM main
VAR
a : USINT;
b : UINT;
c : UDINT;
d : ULINT;
END_VAR
a := ABS(a);
b := ABS(b);
c := ABS(c);
d := ABS(IN := d);
END_PROGRAM",
);

filtered_assert_snapshot!(result, @r#"
; ModuleID = '<internal>'
source_filename = "<internal>"
target datalayout = "[filtered]"
target triple = "[filtered]"

%main = type { i8, i16, i32, i64 }

@main_instance = global %main zeroinitializer

define void @main(ptr %0) {
entry:
%a = getelementptr inbounds nuw %main, ptr %0, i32 0, i32 0
%b = getelementptr inbounds nuw %main, ptr %0, i32 0, i32 1
%c = getelementptr inbounds nuw %main, ptr %0, i32 0, i32 2
%d = getelementptr inbounds nuw %main, ptr %0, i32 0, i32 3
%load_a = load i8, ptr %a, align [filtered]
store i8 %load_a, ptr %a, align [filtered]
%load_b = load i16, ptr %b, align [filtered]
store i16 %load_b, ptr %b, align [filtered]
%load_c = load i32, ptr %c, align [filtered]
store i32 %load_c, ptr %c, align [filtered]
%load_d = load i64, ptr %d, align [filtered]
store i64 %load_d, ptr %d, align [filtered]
ret void
}
"#);
}

#[test]
fn builtin_function_call_sizeof() {
let result = codegen(
Expand Down
15 changes: 10 additions & 5 deletions src/validation/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1986,11 +1986,16 @@ fn validate_call<T: AnnotationMap>(
);
}

// Check if we're dealing with a builtin function and if so call its validation function
if let Some(validation) = builtins::get_builtin(fn_ident.get_flat_reference_name().unwrap_or_default())
.and_then(BuiltIn::get_validation)
{
validation(validator, fn_ident, fn_args, context.annotations, context.index);
// Check if we're dealing with a builtin function and if so call its validation function.
// A generic call can also bind to a builtin under its resolved name (e.g. ABS(UINT#1)
// binds to ABS__UINT), so dispatch the validation of the bound builtin as well.
let flat_name = fn_ident.get_flat_reference_name().unwrap_or_default();
let call_name =
context.annotations.get_call_name(fn_ident).filter(|it| !it.eq_ignore_ascii_case(flat_name));
for name in std::iter::once(flat_name).chain(call_name) {
if let Some(validation) = builtins::get_builtin(name).and_then(BuiltIn::get_validation) {
validation(validator, fn_ident, fn_args, context.annotations, context.index);
}
}

let Some(pou) = context.find_pou(fn_ident) else {
Expand Down
Loading
Loading