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
2 changes: 1 addition & 1 deletion book/technical/participants/06-init.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ An array of a built-in type gets an empty constructor. An array literal that is

### Stack variables

Function and method locals, and `VAR_TEMP` variables in any POU, are initialized at the start of the POU body. These statements use no `self.` prefix. A function also constructs its return value when needed: `FUNCTION useLine: Point` with `localLine: Line` starts with `Line__ctor(localLine);` and `Point__ctor(useLine);`. `VAR_IN_OUT` variables use caller-owned storage and get no constructor call.
Function and method locals and outputs, and `VAR_TEMP` variables in any POU, are initialized at the start of the POU body. These statements use no `self.` prefix. A function also constructs its return value when needed: `FUNCTION useLine: Point` with `localLine: Line` starts with `Line__ctor(localLine);` and `Point__ctor(useLine);`. An output of a function or method is caller-owned storage like a return value, and gets the same treatment: codegen zero-fills it on entry, then the constructor call and the initializer assignment run. `VAR_IN_OUT` variables and variable length array outputs use caller-owned storage that the body must not reset, and get no constructor call.

### Linkage

Expand Down
6 changes: 4 additions & 2 deletions book/technical/pipeline/05-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ In a function, `VAR_IN_OUT` and `VAR_OUTPUT` parameters are pointers, and an agg

Functions are created in two passes, like structs: first a declaration for every POU the unit depends on, including POUs from other units, then the bodies of the POUs declared in this unit. If `scale` were in a second file, the module of `main` would contain `declare i32 @scale(i32, i16, ptr, ptr)` with no body.

A body starts by making every variable addressable, so the statements can treat both kinds alike. A function copies each argument into a stack slot and starts its return variable at zero; a stateful POU computes one pointer per member into the instance:
A body starts by making every variable addressable, so the statements can treat both kinds alike. A function copies each argument into a stack slot, starts its return variable at zero, and starts each output at its initial value, or zero, through the address the caller passed (a variable length array output keeps the caller's bounds and is not reset); a stateful POU computes one pointer per member into the instance:

```llvm
define i32 @scale(i32 %0, i16 %1, ptr %2, ptr %3) {
Expand All @@ -222,6 +222,8 @@ entry:
%overflow = alloca ptr ; output: same
store ptr %3, ptr %overflow
store i32 0, ptr %scale ; the return variable starts at zero
%4 = load ptr, ptr %overflow ; the output starts at zero, in the caller's variable
store i8 0, ptr %4
...
%scale_ret = load i32, ptr %scale ; read the return variable
ret i32 %scale_ret ; and return it
Expand Down Expand Up @@ -316,7 +318,7 @@ call void @llvm.memcpy.p0.p0.i32(ptr align 1 %text, ptr align 1 @utf08_literal_1

### Calls to functions

Codegen places named and positional arguments in parameter order and fills omitted parameters with defaults. It passes scalar inputs by value and `VAR_IN_OUT` and `VAR_OUTPUT` arguments by address. The callee writes through these addresses. For `i := scale(i, factor := 3, total := counter, overflow => flag)`:
Codegen places named and positional arguments in parameter order. A parameter without an argument, which validation allows for a trailing input with a default and for any parameter of a method, and a parameter whose argument is written empty (`overflow =>`), gets its default, or zero; a by-address parameter then receives a temporary holding that value. It passes scalar inputs by value and `VAR_IN_OUT` and `VAR_OUTPUT` arguments by address. The callee writes through these addresses. For `i := scale(i, factor := 3, total := counter, overflow => flag)`:

```llvm
%call = call i32 @scale(i32 %load_i, i16 3, ptr @counter, ptr %flag) ; value, factor, address of total, address of overflow
Expand Down
2 changes: 1 addition & 1 deletion book/user/language/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ END_FUNCTION

`VAR_IN_OUT` is for data that the function reads and writes. It is always the caller's variable.

`VAR_OUTPUT` carries a second result out of the call.
`VAR_OUTPUT` carries a second result out of the call. Every call starts the output at its initial value, or at zero when it has none, exactly like a local variable, so the caller never reads a stale value from a path that does not assign it. An output that is a variable length array, a `REFERENCE TO`, or an alias (`AT`) is the exception: it keeps what the caller passed. The function does this through the caller's variable when it starts, so do not pass one variable as an output and also as a `VAR_IN_OUT` or `VAR_INPUT {ref}` of the same call: the function would read it as zero.


## Calling
Expand Down
2 changes: 1 addition & 1 deletion book/user/language/methods-and-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ END_VAR
value := inlet.Fill(amount := 3);
```

`value` is now `3`, the new level. A method is called like a [function](functions.md#calling): the arguments come by position, in the order of the declaration, or by name. Give every parameter a value, because a parameter that the call leaves out takes the default value of its declaration, and a parameter without a default holds no defined value. The body of the block and its methods share the data of the instance, but a local variable of a method does not survive the call.
`value` is now `3`, the new level. A method is called like a [function](functions.md#calling): the arguments come by position, in the order of the declaration, or by name. A method call may leave out any parameter. The parameter then takes the default value of its declaration, or zero when it has none, and an output that the call leaves out is written to a temporary that the caller never sees. The body of the block and its methods share the data of the instance, but a local variable of a method does not survive the call.

Inside a method, `THIS^` names the instance itself. You need it when a parameter and a member have the same name, because the parameter hides the member:

Expand Down
4 changes: 4 additions & 0 deletions compiler/plc_ast/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,10 @@ impl VariableBlockType {
pub fn is_inout(&self) -> bool {
matches!(self, VariableBlockType::InOut)
}

pub fn is_output(&self) -> bool {
matches!(self, VariableBlockType::Output)
}
}

impl Display for VariableBlockType {
Expand Down
21 changes: 14 additions & 7 deletions compiler/plc_lowering/src/initializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,11 @@ impl AstVisitor for Initializer {
};
let mut stmts = vec![];
let base = Self::get_base_ident(&variable_block_type, is_stateful);
if !variable_block_type.is_inout() {
let is_vla = variable
.data_type_declaration
.get_referenced_type()
.is_some_and(|it| index.get_type_information_or_void(it).is_vla());
if !variable_block_type.is_inout() && !is_vla {
if let Some(constructor) = variable
.data_type_declaration
.get_referenced_type()
Expand Down Expand Up @@ -331,10 +335,7 @@ impl AstVisitor for Initializer {
}
}
}
if variable_block_type.is_temp()
|| (variable_block_type.is_local() && !is_stateful)
|| variable_block_type.is_inout()
{
if Self::is_stack_variable(&variable_block_type, is_stateful) || variable_block_type.is_inout() {
self.add_to_current_stack_constructor(stmts);
} else {
self.add_to_current_constructor(stmts);
Expand Down Expand Up @@ -1032,9 +1033,15 @@ impl Initializer {
None
}

/// Temps, and locals and outputs of a stateless POU, live on the stack and are initialized on
/// every call; an output of a function or method is reset through the caller's address.
fn is_stack_variable(variable_block_type: &VariableBlockType, is_stateful: bool) -> bool {
variable_block_type.is_temp()
|| ((variable_block_type.is_local() || variable_block_type.is_output()) && !is_stateful)
}

fn get_base_ident(variable_block_type: &VariableBlockType, is_stateful: bool) -> Option<&str> {
if variable_block_type.is_temp()
|| (variable_block_type.is_local() && !is_stateful)
if Self::is_stack_variable(variable_block_type, is_stateful)
|| variable_block_type.is_inout()
|| variable_block_type.is_global()
{
Expand Down
66 changes: 28 additions & 38 deletions src/codegen/generators/expression_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1322,22 +1322,16 @@ impl<'ink, 'b> ExpressionCodeGenerator<'ink, 'b> {
declared_parameter: Option<&VariableIndexEntry>,
) -> Result<BasicValueEnum<'ink>, CodegenError> {
if argument.is_empty_statement() {
// Uninitialized var_output / var_in_out
// an explicitly empty by-ref argument gets an initialized temporary, like an omitted one
if let Some(parameter) = declared_parameter {
return self.generate_empty_expression(parameter);
}
let v_type = self
.llvm_index
.find_associated_type(type_name)
.ok_or_else(|| Diagnostic::unknown_type(type_name, argument))?;

let ptr_value = self.llvm.builder.build_alloca(v_type, "")?;
if let Some(p) = declared_parameter {
if let Some(initial_value) =
self.get_initial_value(&p.initial_value, &self.get_parameter_type(p))
{
let value = self.generate_expression(initial_value)?;
self.llvm.builder.build_store(ptr_value, value)?;
}
}

self.llvm.builder.build_store(ptr_value, v_type.const_zero())?;
return Ok(ptr_value.into());
}

Expand Down Expand Up @@ -1595,46 +1589,40 @@ impl<'ink, 'b> ExpressionCodeGenerator<'ink, 'b> {
.find_effective_type_info(parameter.get_type_name())
.is_some_and(|it| it.is_reference_to());

// a missing argument gets its declared initial value, or zero
let initial_value = self.get_initial_value(&parameter.initial_value, &parameter_type_name);

match declaration_type {
ArgumentType::ByVal(..) if parameter_is_reference_to => {
// REFERENCE TO parameters expect an address; pass the address of a zeroed temporary
let ptr_value = self.llvm.builder.build_alloca(parameter_type, "")?;
self.llvm.builder.build_store(ptr_value, parameter_type.const_zero())?;
Ok(ptr_value.as_basic_value_enum())
}
ArgumentType::ByVal(..)
if declaration_type.is_input() && parameter_type_info.is_aggregate_type() =>
{
// Aggregate VAR_INPUT defaults are passed by reference in function signatures.
let ptr_value = self.llvm.builder.build_alloca(parameter_type, "")?;
if let Some(initial_value) =
self.get_initial_value(&parameter.initial_value, &parameter_type_name)
{
let value = self.generate_expression(initial_value)?;
self.llvm.builder.build_store(ptr_value, value)?;
}
let value = match initial_value {
Some(initial_value) => self.generate_expression(initial_value)?,
None => parameter_type.const_zero(),
};
self.llvm.builder.build_store(ptr_value, value)?;
Ok(ptr_value.as_basic_value_enum())
}
ArgumentType::ByVal(..) => {
if let Some(initial_value) =
self.get_initial_value(&parameter.initial_value, &parameter_type_name)
{
self.generate_expression(initial_value)
} else {
let ptr_value = self.llvm.builder.build_alloca(parameter_type, "")?;
let pointee = parameter_type;
Ok(self.llvm.load_pointer(pointee, &ptr_value, "")?)
}
}
ArgumentType::ByVal(..) => match initial_value {
Some(initial_value) => self.generate_expression(initial_value),
None => Ok(parameter_type.const_zero()),
},
_ => {
// by-ref parameters receive the address of an initialized temporary
let ptr_value = self.llvm.builder.build_alloca(parameter_type, "")?;

// if default value is given for an output
// we need to initialize the pointer value before returning
if let Some(initial_value) =
self.get_initial_value(&parameter.initial_value, &parameter_type_name)
{
let value = self.generate_expression(initial_value)?;
self.llvm.builder.build_store(ptr_value, value)?;
}
let value = match initial_value {
Some(initial_value) => self.generate_expression(initial_value)?,
None => parameter_type.const_zero(),
};
self.llvm.builder.build_store(ptr_value, value)?;
Ok(ptr_value.as_basic_value_enum())
}
}
Expand Down Expand Up @@ -1714,7 +1702,9 @@ impl<'ink, 'b> ExpressionCodeGenerator<'ink, 'b> {
.llvm_index
.find_associated_type(inner_type_name)
.ok_or_else(|| Diagnostic::unknown_type(parameter.get_name(), expression))?;
builder.build_alloca(temp_type, "empty_varinout")?.as_basic_value_enum()
let temp = builder.build_alloca(temp_type, "empty_varinout")?;
builder.build_store(temp, temp_type.const_zero())?;
Comment on lines +1705 to +1706

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply type defaults to empty stateful in-out arguments

When a program or function-block call passes an empty VAR_IN_OUT whose target type has a nonzero default, for example TYPE T : DINT := 20 and fb(x := ), this branch always writes LLVM zero into the temporary. The function-call path uses get_initial_value, and the updated codegen documentation promises default-or-zero behavior, so the callee sees 0 instead of 20. Initialize the temporary from the parameter or type default before falling back to zero.

AGENTS.md reference: AGENTS.md:L42-L42

Useful? React with 👍 / 👎.

temp.as_basic_value_enum()
} else {
self.generate_lvalue(expression)?.as_basic_value_enum()
};
Expand Down
59 changes: 59 additions & 0 deletions src/codegen/generators/pou_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ impl<'ink, 'cg> PouGenerator<'ink, 'cg> {
&function_context,
debug,
)?;
self.generate_initialization_of_output_params(&pou_members, &local_index)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the POU internals page for output resets

This new initialization call makes every function or method output start at its default value, but book/technical/internals/00-pous.md still says that only locals and the return variable are initialized and shows scale IR with no store through %overflow. Update that paragraph and IR example so the technical book describes the behavior introduced here.

AGENTS.md reference: AGENTS.md:L42-L42

Useful? React with 👍 / 👎.

} else {
//Generate temp variables
let members = pou_members.into_iter().filter(|it| it.is_temp()).collect::<Vec<_>>();
Expand Down Expand Up @@ -894,6 +895,64 @@ impl<'ink, 'cg> PouGenerator<'ink, 'cg> {
Ok(())
}

/// resets every by-ref output parameter of a function or method to its initial value, so the
/// caller never observes a stale value through an output the body does not assign
fn generate_initialization_of_output_params(
&self,
variables: &[&VariableIndexEntry],
local_llvm_index: &LlvmTypedIndex,
) -> Result<(), CodegenError> {
let exp_gen = ExpressionCodeGenerator::new_context_free(
&self.llvm,
self.index,
self.annotations,
local_llvm_index,
);
let outputs = variables.iter().filter(|it| it.is_output() && it.get_declaration_type().is_by_ref());

for variable in outputs {
let Some(inner_type_name) = self
.index
.find_effective_type_info(variable.get_type_name())
.and_then(|it| it.get_inner_pointer_type_name())
else {
continue;
};
// reference and alias outputs are bound by the body, and a variable length array
// output holds the caller's bounds and data pointer, so neither is reset
if self
.index
.find_effective_type_info(inner_type_name)
.is_some_and(|it| it.is_reference_to() || it.is_alias() || it.is_vla())
{
continue;
}
let Some(pointer_slot) =
local_llvm_index.find_loaded_associated_variable_value(variable.get_qualified_name())
else {
continue;
};

let ptr_type = self.llvm.context.ptr_type(AddressSpace::from(ADDRESS_SPACE_GENERIC));
let output = self.llvm.builder.build_load(ptr_type, pointer_slot, "")?.into_pointer_value();
// only a resolved constant is applied here; any other initializer is assigned by the
// lowered stack initializer, which runs with the instance in scope
let initializer = variable
.initial_value
.as_ref()
.and_then(|id| self.index.get_const_expressions().get_resolved_constant_statement(id));
self.llvm.generate_variable_initializer(
self.llvm_index,
self.index,
(variable.get_qualified_name(), inner_type_name, &variable.source_location),
output,
initializer,
&exp_gen,
)?;
}
Ok(())
}

/// initializes the variable represented by `variable` by storing into the given `variable_to_initialize` pointer using either
/// the optional `initializer_statement` (hence code like: `variable : type := initializer_statement`), or determine the initial
/// value with the help of the `variable`'s index entry by e.g. looking for a default value of the variable's type
Expand Down
6 changes: 4 additions & 2 deletions src/codegen/tests/directaccess_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,9 +551,11 @@ fn direct_access_in_output_assignment_of_function() {
store i8 %0, ptr %in, align [filtered]
%out = alloca ptr, align [filtered]
store ptr %1, ptr %out, align [filtered]
%2 = load ptr, ptr %out, align [filtered]
store i8 0, ptr %2, align [filtered]
%deref = load ptr, ptr %out, align [filtered]
%2 = load i8, ptr %in, align [filtered]
%tmpVar = xor i8 %2, -1
%3 = load i8, ptr %in, align [filtered]
%tmpVar = xor i8 %3, -1
store i8 %tmpVar, ptr %deref, align [filtered]
ret void
}
Expand Down
2 changes: 2 additions & 0 deletions src/codegen/tests/fnptr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ fn function_pointer_method_with_all_variable_parameter_types() {
%inout = alloca ptr, align [filtered]
store ptr %3, ptr %inout, align [filtered]
store i32 0, ptr %A.foo, align [filtered]
%4 = load ptr, ptr %out, align [filtered]
call void @llvm.memset.p0.i64(ptr align [filtered] %4, i8 0, i64 ptrtoint (ptr getelementptr ([81 x i8], ptr null, i32 1) to i64), i1 false)
%A__foo_ret = load i32, ptr %A.foo, align [filtered]
ret i32 %A__foo_ret
}
Expand Down
2 changes: 2 additions & 0 deletions src/codegen/tests/function_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,8 @@ fn function_output_should_be_cast_if_needed() {
%result = alloca ptr, align [filtered]
store ptr %2, ptr %result, align [filtered]
store i16 0, ptr %libFunction, align [filtered]
%3 = load ptr, ptr %result, align [filtered]
store float 0.000000e+00, ptr %3, align [filtered]
%libFunction_ret = load i16, ptr %libFunction, align [filtered]
ret i16 %libFunction_ret
}
Expand Down
Loading
Loading