implements PR016
This commit is contained in:
parent
8d8c3dc180
commit
e4be2a033f
@ -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.
|
|
||||||
@ -902,7 +902,12 @@ public final class PbsParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PbsAst.LValue parseLValue() {
|
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<String>();
|
final var segments = new ArrayList<String>();
|
||||||
var end = root.end();
|
var end = root.end();
|
||||||
while (cursor.match(PbsTokenKind.DOT)) {
|
while (cursor.match(PbsTokenKind.DOT)) {
|
||||||
@ -1057,7 +1062,7 @@ public final class PbsParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAssignmentStatementStart() {
|
private boolean isAssignmentStatementStart() {
|
||||||
if (!cursor.check(PbsTokenKind.IDENTIFIER)) {
|
if (!cursor.check(PbsTokenKind.IDENTIFIER) && !cursor.check(PbsTokenKind.THIS)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,11 @@ final class PbsFlowBodyAnalyzer {
|
|||||||
private final PbsFlowTypeOps typeOps = new PbsFlowTypeOps();
|
private final PbsFlowTypeOps typeOps = new PbsFlowTypeOps();
|
||||||
private final PbsFlowExpressionAnalyzer expressionAnalyzer = new PbsFlowExpressionAnalyzer(typeOps);
|
private final PbsFlowExpressionAnalyzer expressionAnalyzer = new PbsFlowExpressionAnalyzer(typeOps);
|
||||||
|
|
||||||
|
private record AssignmentTargetResolution(
|
||||||
|
TypeView type,
|
||||||
|
boolean assignable) {
|
||||||
|
}
|
||||||
|
|
||||||
public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) {
|
public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) {
|
||||||
final var model = Model.from(ast);
|
final var model = Model.from(ast);
|
||||||
|
|
||||||
@ -136,7 +141,7 @@ final class PbsFlowBodyAnalyzer {
|
|||||||
ExprUse.VALUE,
|
ExprUse.VALUE,
|
||||||
true,
|
true,
|
||||||
this::analyzeBlock);
|
this::analyzeBlock);
|
||||||
scope.bind(letStatement.name(), expected == null ? initializer.type() : expected);
|
scope.bind(letStatement.name(), expected == null ? initializer.type() : expected, !letStatement.isConst());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (statement instanceof PbsAst.ReturnStatement returnStatement) {
|
if (statement instanceof PbsAst.ReturnStatement returnStatement) {
|
||||||
@ -283,10 +288,24 @@ final class PbsFlowBodyAnalyzer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (statement instanceof PbsAst.AssignStatement assignStatement) {
|
if (statement instanceof PbsAst.AssignStatement assignStatement) {
|
||||||
expressionAnalyzer.analyzeExpression(
|
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(),
|
assignStatement.value(),
|
||||||
scope,
|
scope,
|
||||||
null,
|
expectedType,
|
||||||
returnType,
|
returnType,
|
||||||
resultErrorName,
|
resultErrorName,
|
||||||
receiverType,
|
receiverType,
|
||||||
@ -294,7 +313,208 @@ final class PbsFlowBodyAnalyzer {
|
|||||||
diagnostics,
|
diagnostics,
|
||||||
ExprUse.VALUE,
|
ExprUse.VALUE,
|
||||||
true,
|
true,
|
||||||
this::analyzeBlock);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -750,9 +750,16 @@ final class PbsFlowExpressionAnalyzer {
|
|||||||
if (struct == null) {
|
if (struct == null) {
|
||||||
return ExprResult.type(TypeView.unknown());
|
return ExprResult.type(TypeView.unknown());
|
||||||
}
|
}
|
||||||
final var fieldType = struct.fields().get(memberExpr.memberName());
|
final var field = struct.fields().get(memberExpr.memberName());
|
||||||
if (fieldType != null) {
|
if (field != null) {
|
||||||
return ExprResult.type(fieldType);
|
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());
|
final var methods = struct.methods().get(memberExpr.memberName());
|
||||||
if (methods != null && !methods.isEmpty()) {
|
if (methods != null && !methods.isEmpty()) {
|
||||||
@ -825,6 +832,21 @@ final class PbsFlowExpressionAnalyzer {
|
|||||||
return ExprResult.type(TypeView.unknown());
|
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(
|
private TypeView analyzeSwitchExpression(
|
||||||
final PbsAst.SwitchExpr switchExpr,
|
final PbsAst.SwitchExpr switchExpr,
|
||||||
final Scope scope,
|
final Scope scope,
|
||||||
|
|||||||
@ -141,10 +141,16 @@ final class PbsFlowSemanticSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
record StructInfo(
|
record StructInfo(
|
||||||
Map<String, TypeView> fields,
|
Map<String, StructFieldInfo> fields,
|
||||||
Map<String, List<CallableSymbol>> methods) {
|
Map<String, List<CallableSymbol>> methods) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record StructFieldInfo(
|
||||||
|
TypeView type,
|
||||||
|
boolean isPublic,
|
||||||
|
boolean isMutable) {
|
||||||
|
}
|
||||||
|
|
||||||
record ServiceInfo(
|
record ServiceInfo(
|
||||||
Map<String, List<CallableSymbol>> methods) {
|
Map<String, List<CallableSymbol>> methods) {
|
||||||
}
|
}
|
||||||
@ -168,9 +174,14 @@ final class PbsFlowSemanticSupport {
|
|||||||
final var model = new Model();
|
final var model = new Model();
|
||||||
for (final var topDecl : ast.topDecls()) {
|
for (final var topDecl : ast.topDecls()) {
|
||||||
if (topDecl instanceof PbsAst.StructDecl structDecl) {
|
if (topDecl instanceof PbsAst.StructDecl structDecl) {
|
||||||
final var fields = new HashMap<String, TypeView>();
|
final var fields = new HashMap<String, StructFieldInfo>();
|
||||||
for (final var field : structDecl.fields()) {
|
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<String, List<CallableSymbol>>();
|
final var methods = new HashMap<String, List<CallableSymbol>>();
|
||||||
for (final var method : structDecl.methods()) {
|
for (final var method : structDecl.methods()) {
|
||||||
@ -348,7 +359,12 @@ final class PbsFlowSemanticSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static final class Scope {
|
static final class Scope {
|
||||||
private final Map<String, TypeView> names = new HashMap<>();
|
private final Map<String, LocalSymbol> names = new HashMap<>();
|
||||||
|
|
||||||
|
record LocalSymbol(
|
||||||
|
TypeView type,
|
||||||
|
boolean mutable) {
|
||||||
|
}
|
||||||
|
|
||||||
Scope copy() {
|
Scope copy() {
|
||||||
final var scope = new Scope();
|
final var scope = new Scope();
|
||||||
@ -357,11 +373,21 @@ final class PbsFlowSemanticSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void bind(final String name, final TypeView type) {
|
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) {
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,11 @@ public enum PbsSemanticsErrors {
|
|||||||
E_SEM_INVALID_MEMBER_ACCESS,
|
E_SEM_INVALID_MEMBER_ACCESS,
|
||||||
E_SEM_INVALID_THIS_CONTEXT,
|
E_SEM_INVALID_THIS_CONTEXT,
|
||||||
E_SEM_INVALID_SELF_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_NON_BOOL_CONDITION,
|
||||||
E_SEM_IF_BRANCH_TYPE_MISMATCH,
|
E_SEM_IF_BRANCH_TYPE_MISMATCH,
|
||||||
E_SEM_SWITCH_SELECTOR_INVALID,
|
E_SEM_SWITCH_SELECTOR_INVALID,
|
||||||
|
|||||||
@ -125,4 +125,27 @@ class PbsParserStatementsTest {
|
|||||||
|
|
||||||
assertTrue(diagnostics.hasErrors(), "Assignment target with call segment must be rejected");
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user