Skip to content

Commit

Permalink
feat: Introduce REF= and REFERENCE_TO (#1251)
Browse files Browse the repository at this point in the history
This PR introduces two new keywords, namely `REF=` and `REFERENCE TO`:
* `REF=` is essentially syntactic sugar for an assignment where the right-hand side is wrapped in a `REF()` function call. Therefore `foo := REF(bar)` and `foo REF= bar` are equivalent.
* `REFERENCE TO` is identical to `REF_TO` with the exception of being auto-deref by default. A variable `foo` declared as `REFERENCE TO` will therefore auto-deref on assignments, i.e. `foo := 5` is equivalent to `foo^ := 5`.

More information on CodeSys' [REF=](https://help.codesys.com/api-content/2/codesys/3.5.12.0/en/_cds_ref_assignment/) and [REFERENCE TO](https://help.codesys.com/api-content/2/codesys/3.5.12.0/en/_cds_datatype_reference/) documentation pages.
  • Loading branch information
volsa committed Jul 10, 2024
1 parent 4aea520 commit 79abdb6
Show file tree
Hide file tree
Showing 45 changed files with 971 additions and 54 deletions.
34 changes: 29 additions & 5 deletions compiler/plc_ast/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,9 @@ pub enum DataType {
PointerType {
name: Option<String>,
referenced_type: Box<DataTypeDeclaration>,
auto_deref: bool,
/// Denotes whether the variable was declared as `REFERENCE TO`, e.g. `foo : REFERENCE TO DINT`
is_reference_to: bool,
},
StringType {
name: Option<String>,
Expand Down Expand Up @@ -596,11 +599,14 @@ pub struct AstNode {
#[derive(Debug, Clone, PartialEq)]
pub enum AstStatement {
EmptyStatement(EmptyStatement),
// a placeholder that indicates a default value of a datatype

// A placeholder which indicates a default value of a datatype
DefaultValue(DefaultValue),

// Literals
Literal(AstLiteral),
MultipliedStatement(MultipliedStatement),

// Expressions
ReferenceExpr(ReferenceExpr),
Identifier(String),
Expand All @@ -612,15 +618,17 @@ pub enum AstStatement {
ParenExpression(Box<AstNode>),
RangeStatement(RangeStatement),
VlaRangeStatement,
// Assignment

// TODO: Merge these variants with a `kind` field?
// Assignments
Assignment(Assignment),
// OutputAssignment
OutputAssignment(Assignment),
//Call Statement
RefAssignment(Assignment),

CallStatement(CallStatement),

// Control Statements
ControlStatement(AstControlStatement),

CaseCondition(Box<AstNode>),
ExitStatement(()),
ContinueStatement(()),
Expand Down Expand Up @@ -661,6 +669,9 @@ impl Debug for AstNode {
AstStatement::OutputAssignment(Assignment { left, right }) => {
f.debug_struct("OutputAssignment").field("left", left).field("right", right).finish()
}
AstStatement::RefAssignment(Assignment { left, right }) => {
f.debug_struct("ReferenceAssignment").field("left", left).field("right", right).finish()
}
AstStatement::CallStatement(CallStatement { operator, parameters }) => f
.debug_struct("CallStatement")
.field("operator", operator)
Expand Down Expand Up @@ -1319,6 +1330,19 @@ impl AstFactory {
)
}

// TODO: Merge `create_assignment`, `create_output_assignment` and `create_ref_assignment`
// once the the Assignment AstStatements have been merged and a `kind` field is available
// I.e. something like `AstStatement::Assignment { data, kind: AssignmentKind { Normal, Output, Reference } }
// and then fn create_assignment(kind: AssignmentKind, ...)
pub fn create_ref_assignment(left: AstNode, right: AstNode, id: AstId) -> AstNode {
let location = left.location.span(&right.location);
AstNode::new(
AstStatement::RefAssignment(Assignment { left: Box::new(left), right: Box::new(right) }),
id,
location,
)
}

pub fn create_member_reference(member: AstNode, base: Option<AstNode>, id: AstId) -> AstNode {
let location = base
.as_ref()
Expand Down
10 changes: 10 additions & 0 deletions compiler/plc_ast/src/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,15 @@ pub trait AstVisitor: Sized {
stmt.walk(self)
}

/// Visits an `RefAssignment` node.
/// Make sure to call `walk` on the `Assignment` node to visit its children.
/// # Arguments
/// * `stmt` - The unwraped, typed `Assignment` node to visit.
/// * `node` - The wrapped `AstNode` node to visit. Offers access to location information and AstId
fn visit_ref_assignment(&mut self, stmt: &Assignment, _node: &AstNode) {
stmt.walk(self)
}

/// Visits a `CallStatement` node.
/// Make sure to call `walk` on the `CallStatement` node to visit its children.
/// # Arguments
Expand Down Expand Up @@ -556,6 +565,7 @@ impl Walker for AstNode {
AstStatement::VlaRangeStatement => visitor.visit_vla_range_statement(node),
AstStatement::Assignment(stmt) => visitor.visit_assignment(stmt, node),
AstStatement::OutputAssignment(stmt) => visitor.visit_output_assignment(stmt, node),
AstStatement::RefAssignment(stmt) => visitor.visit_ref_assignment(stmt, node),
AstStatement::CallStatement(stmt) => visitor.visit_call_statement(stmt, node),
AstStatement::ControlStatement(stmt) => visitor.visit_control_statement(stmt, node),
AstStatement::CaseCondition(stmt) => visitor.visit_case_condition(stmt, node),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ lazy_static! {
E095, Error, include_str!("./error_codes/E095.md"), // Action call without `()`
E096, Warning, include_str!("./error_codes/E096.md"), // Integer Condition
E097, Error, include_str!("./error_codes/E097.md"), // Invalid Array Range
E098, Error, include_str!("./error_codes/E098.md"), // Invalid `REF=` assignment
E099, Error, include_str!("./error_codes/E099.md"), // Invalid `REFERENCE TO` declaration
);
}

Expand Down
19 changes: 19 additions & 0 deletions compiler/plc_diagnostics/src/diagnostics/error_codes/E098.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Invalid REF= assignment

`REF=` assignments are considered valid if the left-hand side of the assignment is a pointer variable
and the right-hand side is a variable of the type that is being referenced.

For example assignments such as the following are invalid

```smalltalk
VAR
foo : DINT;
bar : DINT;
qux : SINT;
refFoo : REFERENCE TO DINT;
END_VAR
refFoo REF= 5; // `5` is not a variable
foo REF= bar; // `foo` is not a pointer
refFoo REF= qux; // `refFoo` and `qux` have different types, DINT vs SINT
```
8 changes: 8 additions & 0 deletions compiler/plc_diagnostics/src/diagnostics/error_codes/E099.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Invalid `REFERENCE TO` declaration

`REFERENCE TO` variable declarations are considered valid if the referenced type is not of the following form
* `foo : REFERENCE TO REFERENCE TO (* ... *)`
* `foo : ARRAY[...] OF REFERENCE TO (* ... *)`
* `foo : REF_TO REFERENCE TO (* ... *)`

Furthermore `REFERENCE_TO` variables must not be initialized in their declaration, e.g. `foo : REFERENCE TO DINT := bar`.
3 changes: 1 addition & 2 deletions src/codegen/generators/expression_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2507,14 +2507,13 @@ impl<'ink, 'b> ExpressionCodeGenerator<'ink, 'b> {
/// - `access` the ReferenceAccess of the reference to generate
/// - `base` the "previous" segment of an optional qualified reference-access
/// - `original_expression` the original ast-statement used to report Diagnostics
fn generate_reference_expression(
pub(crate) fn generate_reference_expression(
&self,
access: &ReferenceAccess,
base: Option<&AstNode>,
original_expression: &AstNode,
) -> Result<ExpressionValue<'ink>, Diagnostic> {
match (access, base) {

// expressions like `base.member`, or just `member`
(ReferenceAccess::Member(member), base) => {
let base_value = base.map(|it| self.generate_expression_value(it)).transpose()?;
Expand Down
29 changes: 28 additions & 1 deletion src/codegen/generators/statement_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ impl<'a, 'b> StatementCodeGenerator<'a, 'b> {
AstStatement::Assignment(data, ..) => {
self.generate_assignment_statement(&data.left, &data.right)?;
}

AstStatement::RefAssignment(data, ..) => {
self.generate_ref_assignment(&data.left, &data.right)?;
}
AstStatement::ControlStatement(ctl_statement, ..) => {
self.generate_control_statement(ctl_statement)?
}
Expand Down Expand Up @@ -234,6 +236,31 @@ impl<'a, 'b> StatementCodeGenerator<'a, 'b> {
}
}

/// Generates IR for a `REF=` assignment, which is syntactic sugar for an assignment where the
/// right-hand side is wrapped in a `REF(...)` call. Specifically `foo REF= bar` and
/// `foo := REF(bar)` are the same.
///
/// Note: Although somewhat similar to the [`generate_assignment_statement`] function, we can't
/// apply the code here because the left side of a `REF=` assignment is flagged as auto-deref.
/// For `REF=` assignments we don't want (and can't) deref without generating incorrect IR.
pub fn generate_ref_assignment(&self, left: &AstNode, right: &AstNode) -> Result<(), Diagnostic> {
let exp = self.create_expr_generator();
let ref_builtin = self.index.get_builtin_function("REF").expect("REF must exist");

let AstStatement::ReferenceExpr(data) = &left.stmt else {
unreachable!("should be covered by a validation")
};

let left_ptr_val = {
let expr = exp.generate_reference_expression(&data.access, data.base.as_deref(), left)?;
expr.get_basic_value_enum().into_pointer_value()
};
let right_expr_val = ref_builtin.codegen(&exp, &[&right], right.get_location())?;

self.llvm.builder.build_store(left_ptr_val, right_expr_val.get_basic_value_enum());
Ok(())
}

/// generates an assignment statement _left_ := _right_
///
/// `left_statement` the left side of the assignment
Expand Down
124 changes: 124 additions & 0 deletions src/codegen/tests/statement_codegen_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,127 @@ fn floating_point_type_casting() {

insta::assert_snapshot!(result);
}

#[test]
fn ref_assignment() {
let result = codegen(
r#"
FUNCTION main
VAR
a : REF_TO DINT;
b : DINT;
END_VAR
a REF= b;
END_PROGRAM
"#,
);

insta::assert_snapshot!(result, @r###"
; ModuleID = 'main'
source_filename = "main"
define void @main() section "fn-$RUSTY$main:v" {
entry:
%a = alloca i32*, align 8
%b = alloca i32, align 4
store i32* null, i32** %a, align 8
store i32 0, i32* %b, align 4
store i32* %b, i32** %a, align 8
ret void
}
"###);
}

#[test]
fn reference_to_assignment() {
let auto_deref = codegen(
r#"
FUNCTION main
VAR
a : REFERENCE TO DINT;
END_VAR
a := 5;
END_FUNCTION
"#,
);

let manual_deref = codegen(
r#"
FUNCTION main
VAR
a : REF_TO DINT;
END_VAR
a^ := 5;
END_FUNCTION
"#,
);

// We want to assert that `a := 5` and `a^ := 5` yield identical IR
assert_eq!(auto_deref, manual_deref);

insta::assert_snapshot!(auto_deref, @r###"
; ModuleID = 'main'
source_filename = "main"
define void @main() section "fn-$RUSTY$main:v" {
entry:
%a = alloca i32*, align 8
store i32* null, i32** %a, align 8
%deref = load i32*, i32** %a, align 8
store i32 5, i32* %deref, align 4
ret void
}
"###);
}

#[test]
fn reference_to_string_assignment() {
let auto_deref = codegen(
r#"
FUNCTION main
VAR
a : REFERENCE TO STRING;
END_VAR
a := 'hello';
END_FUNCTION
"#,
);

let manual_deref = codegen(
r#"
FUNCTION main
VAR
a : REF_TO STRING;
END_VAR
a^ := 'hello';
END_FUNCTION
"#,
);

// We want to assert that `a := 'hello'` and `a^ := 'hello'` yield identical IR
assert_eq!(auto_deref, manual_deref);

insta::assert_snapshot!(auto_deref, @r###"
; ModuleID = 'main'
source_filename = "main"
@utf08_literal_0 = private unnamed_addr constant [6 x i8] c"hello\00"
define void @main() section "fn-$RUSTY$main:v" {
entry:
%a = alloca [81 x i8]*, align 8
store [81 x i8]* null, [81 x i8]** %a, align 8
%deref = load [81 x i8]*, [81 x i8]** %a, align 8
%0 = bitcast [81 x i8]* %deref to i8*
call void @llvm.memcpy.p0i8.p0i8.i32(i8* align 1 %0, i8* align 1 getelementptr inbounds ([6 x i8], [6 x i8]* @utf08_literal_0, i32 0, i32 0), i32 6, i1 false)
ret void
}
; Function Attrs: argmemonly nofree nounwind willreturn
declare void @llvm.memcpy.p0i8.p0i8.i32(i8* noalias nocapture writeonly, i8* noalias nocapture readonly, i32, i1 immarg) #0
attributes #0 = { argmemonly nofree nounwind willreturn }
"###);
}
1 change: 1 addition & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,7 @@ impl Index {
if segments.is_empty() {
return None;
}

//For the first element, if the context does not contain that element, it is possible that the element is also a global variable
let init = match context {
Some(context) => self
Expand Down
4 changes: 4 additions & 0 deletions src/index/tests/index_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,7 @@ fn pointer_and_in_out_pointer_should_not_conflict() {
name: "__main_x".to_string(),
inner_type_name: "INT".to_string(),
auto_deref: false,
is_reference_to: false,
}
);

Expand All @@ -1264,6 +1265,7 @@ fn pointer_and_in_out_pointer_should_not_conflict() {
name: "__auto_pointer_to_INT".to_string(),
inner_type_name: "INT".to_string(),
auto_deref: true,
is_reference_to: false,
}
);
}
Expand Down Expand Up @@ -1303,6 +1305,7 @@ fn pointer_and_in_out_pointer_should_not_conflict_2() {
name: "__main_x".to_string(),
inner_type_name: "INT".to_string(),
auto_deref: false,
is_reference_to: false,
}
);

Expand All @@ -1314,6 +1317,7 @@ fn pointer_and_in_out_pointer_should_not_conflict_2() {
name: "__auto_pointer_to_INT".to_string(),
inner_type_name: "INT".to_string(),
auto_deref: true,
is_reference_to: false,
}
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ UserTypeDeclaration {
referenced_type: DataTypeReference {
referenced_type: "__foo_inline_pointer_",
},
auto_deref: false,
is_reference_to: false,
},
initializer: None,
scope: Some(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ UserTypeDeclaration {
referenced_type: DataTypeReference {
referenced_type: "INT",
},
auto_deref: false,
is_reference_to: false,
},
initializer: None,
scope: Some(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ UserTypeDeclaration {
referenced_type: DataTypeReference {
referenced_type: "INT",
},
auto_deref: false,
is_reference_to: false,
},
initializer: None,
scope: Some(
Expand Down
Loading

0 comments on commit 79abdb6

Please sign in to comment.