From e4be2a033f6d34e8f968e3136e614a2dbd993d64 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 5 Mar 2026 19:48:23 +0000 Subject: [PATCH] implements PR016 --- ...nment-target-and-field-access-semantics.md | 44 ---- .../studio/compiler/pbs/parser/PbsParser.java | 9 +- .../pbs/semantics/PbsFlowBodyAnalyzer.java | 246 +++++++++++++++++- .../semantics/PbsFlowExpressionAnalyzer.java | 28 +- .../pbs/semantics/PbsFlowSemanticSupport.java | 40 ++- .../pbs/semantics/PbsSemanticsErrors.java | 5 + .../pbs/parser/PbsParserStatementsTest.java | 23 ++ .../semantics/PbsSemanticsAssignmentTest.java | 111 ++++++++ 8 files changed, 437 insertions(+), 69 deletions(-) delete mode 100644 docs/pbs/pull-requests/PR-016-pbs-assignment-target-and-field-access-semantics.md create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsAssignmentTest.java diff --git a/docs/pbs/pull-requests/PR-016-pbs-assignment-target-and-field-access-semantics.md b/docs/pbs/pull-requests/PR-016-pbs-assignment-target-and-field-access-semantics.md deleted file mode 100644 index f6095def..00000000 --- a/docs/pbs/pull-requests/PR-016-pbs-assignment-target-and-field-access-semantics.md +++ /dev/null @@ -1,44 +0,0 @@ -# PR-016 - PBS Assignment Target and Field Access Semantics - -## Briefing - -Assignment validation currently does not enforce target existence, assignability, mutability, or write-access constraints. This PR closes those gaps. - -## Motivation - -Unchecked assignment targets allow invalid programs through static semantics. - -## Target - -- Assignment statement semantic analysis. -- Struct field read/write access validation (`private`, `pub`, `pub mut`). - -## Scope - -- Resolve LValue path against known symbols. -- Validate assignment compatibility and write permissions. -- Emit deterministic diagnostics for invalid target/write access. - -## Method - -- Add LValue resolver in flow semantic analyzer. -- Use struct metadata (`isPublic`, `isMutable`) for access checks. -- Distinguish read-access and write-access diagnostics. - -## Acceptance Criteria - -- Missing assignment target is rejected deterministically. -- Writes to private fields from invalid contexts are rejected. -- Writes to non-`pub mut` external fields are rejected. -- Type mismatch in assignment is reported deterministically. - -## Tests - -- Add assignment tests for local, field, and nested member targets. -- Add negative access tests (`pub` read-only vs `pub mut`). -- Add type-mismatch assignment tests. - -## Non-Goals - -- New mutation syntax. -- Runtime memory model changes. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java index 7372e9b6..9e458608 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java @@ -902,7 +902,12 @@ public final class PbsParser { } private PbsAst.LValue parseLValue() { - final var root = consume(PbsTokenKind.IDENTIFIER, "Expected assignment target identifier"); + final PbsToken root; + if (cursor.match(PbsTokenKind.IDENTIFIER, PbsTokenKind.THIS)) { + root = cursor.previous(); + } else { + root = consume(PbsTokenKind.IDENTIFIER, "Expected assignment target identifier"); + } final var segments = new ArrayList(); var end = root.end(); while (cursor.match(PbsTokenKind.DOT)) { @@ -1057,7 +1062,7 @@ public final class PbsParser { } private boolean isAssignmentStatementStart() { - if (!cursor.check(PbsTokenKind.IDENTIFIER)) { + if (!cursor.check(PbsTokenKind.IDENTIFIER) && !cursor.check(PbsTokenKind.THIS)) { return false; } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java index 01922ec8..e59253ae 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java @@ -12,6 +12,11 @@ final class PbsFlowBodyAnalyzer { private final PbsFlowTypeOps typeOps = new PbsFlowTypeOps(); private final PbsFlowExpressionAnalyzer expressionAnalyzer = new PbsFlowExpressionAnalyzer(typeOps); + private record AssignmentTargetResolution( + TypeView type, + boolean assignable) { + } + public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) { final var model = Model.from(ast); @@ -136,7 +141,7 @@ final class PbsFlowBodyAnalyzer { ExprUse.VALUE, true, this::analyzeBlock); - scope.bind(letStatement.name(), expected == null ? initializer.type() : expected); + scope.bind(letStatement.name(), expected == null ? initializer.type() : expected, !letStatement.isConst()); return; } if (statement instanceof PbsAst.ReturnStatement returnStatement) { @@ -283,18 +288,233 @@ final class PbsFlowBodyAnalyzer { return; } if (statement instanceof PbsAst.AssignStatement assignStatement) { - expressionAnalyzer.analyzeExpression( - assignStatement.value(), - scope, - null, - returnType, - resultErrorName, - receiverType, - model, - diagnostics, - ExprUse.VALUE, - true, - this::analyzeBlock); + analyzeAssignmentStatement(assignStatement, scope, returnType, resultErrorName, receiverType, model, diagnostics); } } + + private void analyzeAssignmentStatement( + final PbsAst.AssignStatement assignStatement, + final Scope scope, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics) { + final var target = resolveAssignmentTarget(assignStatement.target(), scope, receiverType, model, diagnostics); + final var expectedType = target == null ? null : target.type(); + final var valueType = expressionAnalyzer.analyzeExpression( + assignStatement.value(), + scope, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true, + this::analyzeBlock).type(); + + if (target == null || !target.assignable()) { + return; + } + + if (!assignmentCompatible(assignStatement.operator(), target.type(), valueType)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_ASSIGN_TYPE_MISMATCH.name(), + "Assigned value type is not compatible with assignment target", + assignStatement.span()); + } + } + + private AssignmentTargetResolution resolveAssignmentTarget( + final PbsAst.LValue lValue, + final Scope scope, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics) { + final var rootName = lValue.rootName(); + final var rootIsThis = "this".equals(rootName); + if (lValue.pathSegments().isEmpty()) { + if (rootIsThis) { + if (receiverType == null) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_INVALID_THIS_CONTEXT.name(), + "Invalid 'this' usage outside struct/service methods and constructors", + lValue.span()); + } + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_NOT_ASSIGNABLE.name(), + "Assignment target is not assignable", + lValue.span()); + return new AssignmentTargetResolution(TypeView.unknown(), false); + } + + final var localType = scope.resolve(rootName); + if (localType != null) { + if (!scope.isMutable(rootName)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_NOT_ASSIGNABLE.name(), + "Cannot assign to immutable local '%s'".formatted(rootName), + lValue.span()); + return new AssignmentTargetResolution(localType, false); + } + return new AssignmentTargetResolution(localType, true); + } + + if (isKnownNonAssignableRoot(rootName, model)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_NOT_ASSIGNABLE.name(), + "Assignment target is not assignable", + lValue.span()); + return new AssignmentTargetResolution(TypeView.unknown(), false); + } + + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_UNRESOLVED.name(), + "Assignment target '%s' does not resolve".formatted(rootName), + lValue.span()); + return new AssignmentTargetResolution(TypeView.unknown(), false); + } + + TypeView currentType; + var currentReceiverIsThis = rootIsThis; + if (rootIsThis) { + if (receiverType == null) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_INVALID_THIS_CONTEXT.name(), + "Invalid 'this' usage outside struct/service methods and constructors", + lValue.span()); + return new AssignmentTargetResolution(TypeView.unknown(), false); + } + currentType = receiverType; + } else { + final var localType = scope.resolve(rootName); + if (localType != null) { + currentType = localType; + } else if (model.serviceSingletons.containsKey(rootName)) { + currentType = model.serviceSingletons.get(rootName); + } else if (model.constTypes.containsKey(rootName)) { + currentType = model.constTypes.get(rootName); + } else { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_UNRESOLVED.name(), + "Assignment target '%s' does not resolve".formatted(rootName), + lValue.span()); + return new AssignmentTargetResolution(TypeView.unknown(), false); + } + } + + for (int i = 0; i < lValue.pathSegments().size(); i++) { + final var segment = lValue.pathSegments().get(i); + final var isLastSegment = i == lValue.pathSegments().size() - 1; + if (currentType.kind() != PbsFlowSemanticSupport.Kind.STRUCT) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(), + "Assignment target segment '%s' is not a struct field".formatted(segment), + lValue.span()); + return new AssignmentTargetResolution(TypeView.unknown(), false); + } + + final var struct = model.structs.get(currentType.name()); + if (struct == null) { + return new AssignmentTargetResolution(TypeView.unknown(), false); + } + final var field = struct.fields().get(segment); + if (field == null) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(), + "Struct member '%s' does not exist".formatted(segment), + lValue.span()); + return new AssignmentTargetResolution(TypeView.unknown(), false); + } + + final var ownerContext = receiverType != null + && receiverType.kind() == PbsFlowSemanticSupport.Kind.STRUCT + && receiverType.name().equals(currentType.name()); + if (isLastSegment) { + if (!canWriteStructField(field, ownerContext, currentReceiverIsThis)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_FIELD_WRITE_ACCESS_DENIED.name(), + "Write access to struct field '%s' is not permitted".formatted(segment), + lValue.span()); + return new AssignmentTargetResolution(field.type(), false); + } + return new AssignmentTargetResolution(field.type(), true); + } + + if (!canReadStructField(field, ownerContext, currentReceiverIsThis)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_FIELD_READ_ACCESS_DENIED.name(), + "Read access to struct field '%s' is not permitted".formatted(segment), + lValue.span()); + return new AssignmentTargetResolution(field.type(), false); + } + + currentType = field.type(); + currentReceiverIsThis = false; + } + + return new AssignmentTargetResolution(TypeView.unknown(), false); + } + + private boolean assignmentCompatible( + final PbsAst.AssignOperator operator, + final TypeView targetType, + final TypeView valueType) { + if (operator == PbsAst.AssignOperator.ASSIGN) { + return typeOps.compatible(valueType, targetType); + } + + final String binaryOperator = switch (operator) { + case ADD_ASSIGN -> "+"; + case SUB_ASSIGN -> "-"; + case MUL_ASSIGN -> "*"; + case DIV_ASSIGN -> "/"; + case MOD_ASSIGN -> "%"; + case ASSIGN -> "="; + }; + if ("=".equals(binaryOperator)) { + return typeOps.compatible(valueType, targetType); + } + final var resultType = typeOps.inferBinaryResult(binaryOperator, targetType, valueType); + return typeOps.compatible(resultType, targetType); + } + + private boolean canReadStructField( + final PbsFlowSemanticSupport.StructFieldInfo field, + final boolean ownerContext, + final boolean receiverIsThis) { + if (field.isPublic()) { + return true; + } + return ownerContext && receiverIsThis; + } + + private boolean canWriteStructField( + final PbsFlowSemanticSupport.StructFieldInfo field, + final boolean ownerContext, + final boolean receiverIsThis) { + if (!field.isPublic()) { + return ownerContext && receiverIsThis; + } + if (ownerContext) { + return true; + } + return field.isMutable(); + } + + private boolean isKnownNonAssignableRoot( + final String rootName, + final Model model) { + return model.serviceSingletons.containsKey(rootName) + || model.constTypes.containsKey(rootName) + || model.topLevelCallables.containsKey(rootName) + || model.callbacks.containsKey(rootName) + || model.enums.containsKey(rootName) + || model.errors.containsKey(rootName) + || model.structs.containsKey(rootName) + || model.services.containsKey(rootName) + || model.contracts.containsKey(rootName); + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowExpressionAnalyzer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowExpressionAnalyzer.java index d0170b8f..f53bcb82 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowExpressionAnalyzer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowExpressionAnalyzer.java @@ -750,9 +750,16 @@ final class PbsFlowExpressionAnalyzer { if (struct == null) { return ExprResult.type(TypeView.unknown()); } - final var fieldType = struct.fields().get(memberExpr.memberName()); - if (fieldType != null) { - return ExprResult.type(fieldType); + final var field = struct.fields().get(memberExpr.memberName()); + if (field != null) { + if (!canReadStructField(field, receiver, memberExpr.receiver(), receiverType)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_FIELD_READ_ACCESS_DENIED.name(), + "Read access to struct field '%s' is not permitted".formatted(memberExpr.memberName()), + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + return ExprResult.type(field.type()); } final var methods = struct.methods().get(memberExpr.memberName()); if (methods != null && !methods.isEmpty()) { @@ -825,6 +832,21 @@ final class PbsFlowExpressionAnalyzer { return ExprResult.type(TypeView.unknown()); } + private boolean canReadStructField( + final PbsFlowSemanticSupport.StructFieldInfo field, + final TypeView receiver, + final PbsAst.Expression receiverExpr, + final TypeView currentReceiverType) { + if (field.isPublic()) { + return true; + } + return receiverExpr instanceof PbsAst.ThisExpr + && currentReceiverType != null + && currentReceiverType.kind() == Kind.STRUCT + && receiver.kind() == Kind.STRUCT + && receiver.name().equals(currentReceiverType.name()); + } + private TypeView analyzeSwitchExpression( final PbsAst.SwitchExpr switchExpr, final Scope scope, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java index 2ca1a8d6..a4b7cd3f 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java @@ -141,9 +141,15 @@ final class PbsFlowSemanticSupport { } record StructInfo( - Map fields, + Map fields, Map> methods) { } + + record StructFieldInfo( + TypeView type, + boolean isPublic, + boolean isMutable) { + } record ServiceInfo( Map> methods) { @@ -168,9 +174,14 @@ final class PbsFlowSemanticSupport { final var model = new Model(); for (final var topDecl : ast.topDecls()) { if (topDecl instanceof PbsAst.StructDecl structDecl) { - final var fields = new HashMap(); + final var fields = new HashMap(); for (final var field : structDecl.fields()) { - fields.put(field.name(), model.typeFrom(field.typeRef())); + fields.put( + field.name(), + new StructFieldInfo( + model.typeFrom(field.typeRef()), + field.isPublic(), + field.isMutable())); } final var methods = new HashMap>(); for (final var method : structDecl.methods()) { @@ -348,7 +359,12 @@ final class PbsFlowSemanticSupport { } static final class Scope { - private final Map names = new HashMap<>(); + private final Map names = new HashMap<>(); + + record LocalSymbol( + TypeView type, + boolean mutable) { + } Scope copy() { final var scope = new Scope(); @@ -357,11 +373,21 @@ final class PbsFlowSemanticSupport { } void bind(final String name, final TypeView type) { - names.put(name, type); + bind(name, type, true); } - + + void bind(final String name, final TypeView type, final boolean mutable) { + names.put(name, new LocalSymbol(type, mutable)); + } + TypeView resolve(final String name) { - return names.get(name); + final var symbol = names.get(name); + return symbol == null ? null : symbol.type(); + } + + boolean isMutable(final String name) { + final var symbol = names.get(name); + return symbol != null && symbol.mutable(); } } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java index 69ba9c00..08a32be8 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java @@ -20,6 +20,11 @@ public enum PbsSemanticsErrors { E_SEM_INVALID_MEMBER_ACCESS, E_SEM_INVALID_THIS_CONTEXT, E_SEM_INVALID_SELF_CONTEXT, + E_SEM_ASSIGN_TARGET_UNRESOLVED, + E_SEM_ASSIGN_TARGET_NOT_ASSIGNABLE, + E_SEM_ASSIGN_TYPE_MISMATCH, + E_SEM_FIELD_READ_ACCESS_DENIED, + E_SEM_FIELD_WRITE_ACCESS_DENIED, E_SEM_IF_NON_BOOL_CONDITION, E_SEM_IF_BRANCH_TYPE_MISMATCH, E_SEM_SWITCH_SELECTOR_INVALID, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserStatementsTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserStatementsTest.java index ddec3d29..d9b1adf7 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserStatementsTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserStatementsTest.java @@ -125,4 +125,27 @@ class PbsParserStatementsTest { assertTrue(diagnostics.hasErrors(), "Assignment target with call segment must be rejected"); } + + @Test + void shouldParseThisAsAssignmentTargetRoot() { + final var source = """ + declare struct Box(pub mut value: int) { + fn set(v: int) -> void { + this.value = v; + return; + } + } + """; + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.isEmpty(), "Assignment target rooted at 'this' should parse"); + + final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().getFirst()); + final var setMethod = structDecl.methods().getFirst(); + final var assignment = assertInstanceOf(PbsAst.AssignStatement.class, setMethod.body().statements().getFirst()); + assertEquals("this", assignment.target().rootName()); + assertEquals(1, assignment.target().pathSegments().size()); + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsAssignmentTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsAssignmentTest.java new file mode 100644 index 00000000..ce627a17 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsAssignmentTest.java @@ -0,0 +1,111 @@ +package p.studio.compiler.pbs.semantics; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.pbs.PbsFrontendCompiler; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class PbsSemanticsAssignmentTest { + + @Test + void shouldAllowLocalAndNestedFieldAssignments() { + final var source = """ + declare struct Inner(pub mut value: int); + declare struct Outer(pub mut inner: Inner) { + fn reset(v: int) -> void { + this.inner.value = v; + return; + } + } + + fn update(o: Outer, next: int) -> int { + let current: int = 0; + current = next; + o.inner.value = current; + return current; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_UNRESOLVED.name()))); + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_NOT_ASSIGNABLE.name()))); + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TYPE_MISMATCH.name()))); + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_FIELD_WRITE_ACCESS_DENIED.name()))); + } + + @Test + void shouldRejectUnresolvedImmutableAndTypeMismatchedAssignments() { + final var source = """ + fn bad(v: int) -> int { + let const fixed: int = 1; + fixed = v; + missing = v; + let n: int = 0; + n = true; + return n; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + final var unresolvedCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_UNRESOLVED.name())) + .count(); + final var immutableCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_NOT_ASSIGNABLE.name())) + .count(); + final var mismatchCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TYPE_MISMATCH.name())) + .count(); + + assertEquals(1, unresolvedCount); + assertEquals(1, immutableCount); + assertEquals(1, mismatchCount); + } + + @Test + void shouldEnforceFieldReadAndWriteAccessRules() { + final var source = """ + declare struct Data(pub mut open: int, pub ro: int, hidden: int) { + fn own(v: int) -> int { + this.hidden = v; + this.ro = v; + return this.hidden + this.ro + this.open; + } + } + + fn outside(d: Data) -> int { + let a: int = d.open; + let b: int = d.ro; + let c: int = d.hidden; + d.open = 1; + d.ro = 2; + d.hidden = 3; + return a + b + c; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + final var readDeniedCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_FIELD_READ_ACCESS_DENIED.name())) + .count(); + final var writeDeniedCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_FIELD_WRITE_ACCESS_DENIED.name())) + .count(); + + assertEquals(1, readDeniedCount); + assertEquals(2, writeDeniedCount); + } +}