From cb1256fe93d237b76353dafaffc5a1305adff72e Mon Sep 17 00:00:00 2001 From: Periapsises <94928308+Periapsises@users.noreply.github.com> Date: Thu, 27 Aug 2026 22:13:43 -0400 Subject: [PATCH 1/4] Add table literal syntax --- Crater/Compilation/Compiler.cs | 44 +++++++++++++--- Crater/CraterParser.g4 | 16 +++++- Crater/SemanticAnalysis/SemanticAnalyzer.cs | 58 +++++++++++++++------ Crater/SyntaxTree/Nodes.cs | 10 +++- Crater/SyntaxTree/SyntaxTreeConverter.cs | 41 +++++++++++++-- 5 files changed, 141 insertions(+), 28 deletions(-) diff --git a/Crater/Compilation/Compiler.cs b/Crater/Compilation/Compiler.cs index c87f60f..3281350 100644 --- a/Crater/Compilation/Compiler.cs +++ b/Crater/Compilation/Compiler.cs @@ -321,8 +321,8 @@ private void CompileExpression(Expression expression) case BooleanLiteral booleanLiteral: CompileBooleanLiteral(booleanLiteral); break; - case ArrayLiteral arrayLiteral: - CompileArrayLiteral(arrayLiteral); + case TableLiteral tableLiteral: + CompileTableLiteral(tableLiteral); break; case NilLiteral nilLiteral: CompileNilLiteral(nilLiteral); @@ -369,21 +369,53 @@ private void CompileBooleanLiteral(BooleanLiteral booleanLiteral) _writer.Write(booleanLiteral.value); } - private void CompileArrayLiteral(ArrayLiteral arrayLiteral) + private void CompileTableLiteral(TableLiteral tableLiteral) { _writer.Write("{"); - for (var i = 0; i < arrayLiteral.values.Count; i++) + for (var i = 0; i < tableLiteral.values.Count; i++) { - CompileExpression(arrayLiteral.values[i]); + switch (tableLiteral.values[i]) + { + case StringIndexedField stringIndexedField: + CompileStringIndexedField(stringIndexedField); + break; + case ValueIndexedField valueIndexedField: + CompileValueIndexedField(valueIndexedField); + break; + case ArrayField arrayField: + CompileArrayField(arrayField); + break; + default: + throw new SwitchExpressionException(tableLiteral.values[i]); + } - if (i < arrayLiteral.values.Count - 1) + if (i < tableLiteral.values.Count - 1) _writer.Write(", "); } _writer.Write("}"); } + private void CompileStringIndexedField(StringIndexedField stringIndexedField) + { + _writer.Write($"{stringIndexedField.index} = "); + CompileExpression(stringIndexedField.value); + } + + private void CompileValueIndexedField(ValueIndexedField valueIndexedField) + { + _writer.Write("["); + CompileExpression(valueIndexedField.index); + _writer.Write("] = "); + CompileExpression(valueIndexedField.value); + } + + private void CompileArrayField(ArrayField arrayField) + { + CompileExpression(arrayField.value); + } + private void CompileNilLiteral(NilLiteral nilLiteral) { _writer.Write(nilLiteral.value); diff --git a/Crater/CraterParser.g4 b/Crater/CraterParser.g4 index 80a7142..4d46c01 100644 --- a/Crater/CraterParser.g4 +++ b/Crater/CraterParser.g4 @@ -75,7 +75,7 @@ expression | left=expression logicalOperator right=expression # LogicalOperation | left=expression AND right=expression # AndOperation | left=expression OR right=expression # OrOperation - | LBRACKET expressionList? RBRACKET # ArrayLiteral + | LBRACKET tableValues? RBRACKET # TableLiteral | NUMBER # NumberLiteral | STRING # StringLiteral | (TRUE | FALSE) # BooleanLiteral @@ -97,6 +97,20 @@ postfixFunctionCall: LPAREN expressionList? RPAREN; postfixBracketIndexing: LSQRBRACKET expression RSQRBRACKET; +tableValues: tableValue (COMMA tableValue)* COMMA?; + +tableValue + : stringIndexedField + | valueIndexedField + | arrayField + ; + +stringIndexedField: IDENTIFIER ASSIGN expression; + +valueIndexedField: LSQRBRACKET index=expression RSQRBRACKET ASSIGN value=expression; + +arrayField: expression; + logicalOperator : EQUAL | NOT_EQUAL diff --git a/Crater/SemanticAnalysis/SemanticAnalyzer.cs b/Crater/SemanticAnalysis/SemanticAnalyzer.cs index d8c6faf..57fed1e 100644 --- a/Crater/SemanticAnalysis/SemanticAnalyzer.cs +++ b/Crater/SemanticAnalysis/SemanticAnalyzer.cs @@ -421,7 +421,7 @@ private List AnalyzeExpression(Expression expression) NumberLiteral numberLiteral => AnalyzeNumberLiteral(numberLiteral), StringLiteral stringLiteral => AnalyzeStringLiteral(stringLiteral), BooleanLiteral booleanLiteral => AnalyzeBooleanLiteral(booleanLiteral), - ArrayLiteral arrayLiteral => AnalyzeArrayLiteral(arrayLiteral), + TableLiteral tableLiteral => AnalyzeTableLiteral(tableLiteral), NilLiteral nilLiteral => AnalyzeNilLiteral(nilLiteral), ParenthesizedExpression parenthesizedExpression => AnalyzeParenthesizedExpression(parenthesizedExpression), VariableReference variableReference => AnalyzeVariableReference(variableReference), @@ -497,29 +497,57 @@ private List AnalyzeBooleanLiteral(BooleanLiteral booleanLiteral) return [TypeRegistry.BooleanType]; } - private List AnalyzeArrayLiteral(ArrayLiteral arrayLiteral) + private List AnalyzeTableLiteral(TableLiteral tableLiteral) { - Type? common = null; + if (tableLiteral.values.Count == 0) + return [new EmptyArrayType()]; - var values = ExpandExpressionList(arrayLiteral.values); - foreach (var value in values) + var arrayValues = new List(); + var keyedFields = new List(); + + foreach (var value in tableLiteral.values) { - if (common == null) - common = value.Item1; - else - common = Type.GetCommonType(common, value.Item1); + switch (value) + { + case ArrayField field: + arrayValues.Add(field.value); + break; + case StringIndexedField or ValueIndexedField: + keyedFields.Add(value); + break; + default: + throw new SwitchExpressionException(value); + } + } + + if (arrayValues.Count > 0 && keyedFields.Count > 0) + { + _reporter.Report(new Diagnostic(TypeErrors.FailedTypeInference, "Cannot mix array values and keyed fields in a table literal", DiagnosticSeverity.Error, tableLiteral.source)); + return [TypeRegistry.UnknownType]; + } + + if (keyedFields.Count > 0) + // TODO: Implement table type + return [TypeRegistry.UnknownType]; + + return AnalyzeArrayValues(arrayValues, tableLiteral.source); + } + + private List AnalyzeArrayValues(List values, Source source) + { + Type? common = null; + foreach (var (type, valueSource) in ExpandExpressionList(values)) + { + common = common == null ? type : Type.GetCommonType(common, type); if (common == null) { - _reporter.Report(new Diagnostic(TypeErrors.FailedTypeInference, $"Could not find a common type for array initializer", DiagnosticSeverity.Error, arrayLiteral.source)); - break; + _reporter.Report(new Diagnostic(TypeErrors.FailedTypeInference, "Could not find a common type for array initializer", DiagnosticSeverity.Error, source)); + return [TypeRegistry.UnknownType]; } } - if (common == null) - return [new EmptyArrayType()]; - - return [new ArrayType(common, TypeRegistry.AnyType)]; + return [new ArrayType(common!, TypeRegistry.AnyType)]; } private List AnalyzeNilLiteral(NilLiteral nilLiteral) diff --git a/Crater/SyntaxTree/Nodes.cs b/Crater/SyntaxTree/Nodes.cs index 01da787..b53b5aa 100644 --- a/Crater/SyntaxTree/Nodes.cs +++ b/Crater/SyntaxTree/Nodes.cs @@ -66,6 +66,14 @@ public sealed record StringLiteral(string value, Source source) : Expression(sou public sealed record BooleanLiteral(string value, Source source) : Expression(source); -public sealed record ArrayLiteral(List values, Source source) : Expression(source); +public sealed record TableLiteral(List values, Source source) : Expression(source); + +public record TableValue(Source source) : Node(source); + +public sealed record StringIndexedField(string index, Expression value, Source source) : TableValue(source); + +public sealed record ValueIndexedField(Expression index, Expression value, Source source) : TableValue(source); + +public sealed record ArrayField(Expression value, Source source) : TableValue(source); public sealed record NilLiteral(string value, Source source) : Expression(source); diff --git a/Crater/SyntaxTree/SyntaxTreeConverter.cs b/Crater/SyntaxTree/SyntaxTreeConverter.cs index b365006..5306b74 100644 --- a/Crater/SyntaxTree/SyntaxTreeConverter.cs +++ b/Crater/SyntaxTree/SyntaxTreeConverter.cs @@ -346,13 +346,13 @@ public override object VisitBooleanLiteral(CraterParser.BooleanLiteralContext co return new BooleanLiteral(context.GetText(), Source.FromContext(context)); } - public override object VisitArrayLiteral(CraterParser.ArrayLiteralContext context) + public override object VisitTableLiteral(CraterParser.TableLiteralContext context) { - if (context.expressionList() == null) - return new ArrayLiteral([], Source.FromContext(context)); + if (context.tableValues() == null) + return new TableLiteral([], Source.FromContext(context)); - var values = Get>(context.expressionList()); - return new ArrayLiteral(values, Source.FromContext(context)); + var values = Get>(context.tableValues()); + return new TableLiteral(values, Source.FromContext(context)); } public override object VisitNilLiteral(CraterParser.NilLiteralContext context) @@ -381,6 +381,37 @@ public override object VisitVariableReference(CraterParser.VariableReferenceCont return new VariableReference(name, Source.FromContext(context)); } + public override object VisitTableValues(CraterParser.TableValuesContext context) + { + var values = new List(); + foreach (var tableValueContext in context.tableValue()) + values.Add(Get(tableValueContext)); + + return values; + } + + public override object VisitStringIndexedField(CraterParser.StringIndexedFieldContext context) + { + var index = context.IDENTIFIER().GetText(); + var value = Get(context.expression()); + + return new StringIndexedField(index, value, Source.FromContext(context)); + } + + public override object VisitValueIndexedField(CraterParser.ValueIndexedFieldContext context) + { + var index = Get(context.index); + var value = Get(context.value); + + return new ValueIndexedField(index, value, Source.FromContext(context)); + } + + public override object VisitArrayField(CraterParser.ArrayFieldContext context) + { + var value = Get(context.expression()); + return new ArrayField(value, Source.FromContext(context)); + } + private Expression BuildPostfixExpression(CraterParser.PostfixExpressionContext context, Expression prefix) { if (context.postfixFunctionCall() is { } postfixFunctionCallContext) From 00432512cef586f585a140236eb96a60c4809c0a Mon Sep 17 00:00:00 2001 From: Periapsises <94928308+Periapsises@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:37:27 -0400 Subject: [PATCH 2/4] Add array type back and enforce string keys in tables --- Crater/Compilation/Compiler.cs | 45 +++++------ Crater/CraterParser.g4 | 20 ++--- Crater/SemanticAnalysis/SemanticAnalyzer.cs | 82 +++++++++++++++------ Crater/SemanticAnalysis/Types/TableType.cs | 34 +++++++++ Crater/SyntaxTree/Nodes.cs | 10 +-- Crater/SyntaxTree/SyntaxTreeConverter.cs | 38 +++++----- 6 files changed, 142 insertions(+), 87 deletions(-) create mode 100644 Crater/SemanticAnalysis/Types/TableType.cs diff --git a/Crater/Compilation/Compiler.cs b/Crater/Compilation/Compiler.cs index 3281350..77ca610 100644 --- a/Crater/Compilation/Compiler.cs +++ b/Crater/Compilation/Compiler.cs @@ -324,6 +324,9 @@ private void CompileExpression(Expression expression) case TableLiteral tableLiteral: CompileTableLiteral(tableLiteral); break; + case ArrayLiteral arrayLiteral: + CompileArrayLiteral(arrayLiteral); + break; case NilLiteral nilLiteral: CompileNilLiteral(nilLiteral); break; @@ -375,20 +378,10 @@ private void CompileTableLiteral(TableLiteral tableLiteral) for (var i = 0; i < tableLiteral.values.Count; i++) { - switch (tableLiteral.values[i]) - { - case StringIndexedField stringIndexedField: - CompileStringIndexedField(stringIndexedField); - break; - case ValueIndexedField valueIndexedField: - CompileValueIndexedField(valueIndexedField); - break; - case ArrayField arrayField: - CompileArrayField(arrayField); - break; - default: - throw new SwitchExpressionException(tableLiteral.values[i]); - } + var value = tableLiteral.values[i]; + + _writer.Write($"{value.index} = "); + CompileExpression(value.value); if (i < tableLiteral.values.Count - 1) _writer.Write(", "); @@ -397,23 +390,19 @@ private void CompileTableLiteral(TableLiteral tableLiteral) _writer.Write("}"); } - private void CompileStringIndexedField(StringIndexedField stringIndexedField) + private void CompileArrayLiteral(ArrayLiteral arrayLiteral) { - _writer.Write($"{stringIndexedField.index} = "); - CompileExpression(stringIndexedField.value); - } + _writer.Write("{"); - private void CompileValueIndexedField(ValueIndexedField valueIndexedField) - { - _writer.Write("["); - CompileExpression(valueIndexedField.index); - _writer.Write("] = "); - CompileExpression(valueIndexedField.value); - } + for (var i = 0; i < arrayLiteral.values.Count; i++) + { + CompileExpression(arrayLiteral.values[i]); - private void CompileArrayField(ArrayField arrayField) - { - CompileExpression(arrayField.value); + if (i < arrayLiteral.values.Count - 1) + _writer.Write(", "); + } + + _writer.Write("}"); } private void CompileNilLiteral(NilLiteral nilLiteral) diff --git a/Crater/CraterParser.g4 b/Crater/CraterParser.g4 index 4d46c01..7c72442 100644 --- a/Crater/CraterParser.g4 +++ b/Crater/CraterParser.g4 @@ -62,7 +62,12 @@ breakStatement: BREAK; typeName: typeName (QMARK | LSQRBRACKET RSQRBRACKET) | primaryType; -primaryType: IDENTIFIER; +primaryType + : IDENTIFIER # NamedType + | tableDefinition # TableType + ; + +tableDefinition: LBRACKET (variableDeclarator (COMMA variableDeclarator)*)? RBRACKET; expressionList: expression (COMMA expression)*; @@ -76,6 +81,7 @@ expression | left=expression AND right=expression # AndOperation | left=expression OR right=expression # OrOperation | LBRACKET tableValues? RBRACKET # TableLiteral + | LBRACKET expressionList? RBRACKET # ArrayLiteral | NUMBER # NumberLiteral | STRING # StringLiteral | (TRUE | FALSE) # BooleanLiteral @@ -99,17 +105,7 @@ postfixBracketIndexing: LSQRBRACKET expression RSQRBRACKET; tableValues: tableValue (COMMA tableValue)* COMMA?; -tableValue - : stringIndexedField - | valueIndexedField - | arrayField - ; - -stringIndexedField: IDENTIFIER ASSIGN expression; - -valueIndexedField: LSQRBRACKET index=expression RSQRBRACKET ASSIGN value=expression; - -arrayField: expression; +tableValue: IDENTIFIER ASSIGN expression; logicalOperator : EQUAL diff --git a/Crater/SemanticAnalysis/SemanticAnalyzer.cs b/Crater/SemanticAnalysis/SemanticAnalyzer.cs index 57fed1e..fe9a2b9 100644 --- a/Crater/SemanticAnalysis/SemanticAnalyzer.cs +++ b/Crater/SemanticAnalysis/SemanticAnalyzer.cs @@ -115,6 +115,8 @@ private void AnalyzeVariableDeclaration(VariableDeclaration variableDeclaration) { if (initializer.Item1 is NilType) _reporter.Report(new Diagnostic(TypeErrors.NilAssignment, $"Cannot assign nil to '{name}' as it is declared with the non-nullable type '{type}'", DiagnosticSeverity.Error, variableDeclaration.source)); + else if (type is TableType && initializer.Item1 is TableType) + _reporter.Report(new Diagnostic(TypeErrors.TypeMismatch, $"The contents of the assigned table does not match the required declaration", DiagnosticSeverity.Error, initializer.Item2)); else _reporter.Report(new Diagnostic(TypeErrors.TypeMismatch, $"The value assigned to '{name}' of type '{initializer.Item1}' is incompatible with the declared type of '{type}'", DiagnosticSeverity.Error, variableDeclaration.source)); } @@ -255,7 +257,10 @@ private void AnalyzeAssignment(Assignment assignment) } else if (!variableType.CanHold(valueType.Item1)) { - _reporter.Report(new Diagnostic(TypeErrors.TypeMismatch, $"Cannot assign value of type '{valueType.Item1}' to variable of type '{variableType}'", DiagnosticSeverity.Error, valueType.Item2)); + if (variableType is TableType && valueType.Item1 is TableType) + _reporter.Report(new Diagnostic(TypeErrors.TypeMismatch, $"The contents of the assigned table do not match the declared type of '{variable}'", DiagnosticSeverity.Error, valueType.Item2)); + else + _reporter.Report(new Diagnostic(TypeErrors.TypeMismatch, $"Cannot assign value of type '{valueType.Item1}' to variable of type '{variableType}'", DiagnosticSeverity.Error, valueType.Item2)); } } } @@ -386,6 +391,7 @@ private Type AnalyzeTypeName(TypeName typeName) NullableTypeName nullableTypeName => AnalyzeNullableTypeName(nullableTypeName), ArrayTypeName arrayTypeName => AnalyzeArrayTypeName(arrayTypeName), NamedTypeName namedTypeName => AnalyzeNamedTypeName(namedTypeName), + TableTypeName tableTypeName => AnalyzeTableTypeName(tableTypeName), _ => throw new SwitchExpressionException(typeName) }; } @@ -412,6 +418,24 @@ private Type AnalyzeNamedTypeName(NamedTypeName namedTypeName) return TypeRegistry.UnknownType; } + private Type AnalyzeTableTypeName(TableTypeName tableTypeName) + { + var fields = new Dictionary(); + foreach (var field in tableTypeName.fields) + { + var type = AnalyzeTypeName(field.type); + + if (fields.TryAdd(field.name, type)) + continue; + + // TODO: Error code for duplicate field + _reporter.Report(new Diagnostic("0", $"Duplicate field '{field.name}' in table type definition", DiagnosticSeverity.Error, field.source)); + fields[field.name] = TypeRegistry.UnknownType; + } + + return new TableType(fields, TypeRegistry.AnyType); + } + private List AnalyzeExpression(Expression expression) { return expression switch @@ -422,6 +446,7 @@ private List AnalyzeExpression(Expression expression) StringLiteral stringLiteral => AnalyzeStringLiteral(stringLiteral), BooleanLiteral booleanLiteral => AnalyzeBooleanLiteral(booleanLiteral), TableLiteral tableLiteral => AnalyzeTableLiteral(tableLiteral), + ArrayLiteral arrayLiteral => AnalyzeArrayLiteral(arrayLiteral), NilLiteral nilLiteral => AnalyzeNilLiteral(nilLiteral), ParenthesizedExpression parenthesizedExpression => AnalyzeParenthesizedExpression(parenthesizedExpression), VariableReference variableReference => AnalyzeVariableReference(variableReference), @@ -499,38 +524,47 @@ private List AnalyzeBooleanLiteral(BooleanLiteral booleanLiteral) private List AnalyzeTableLiteral(TableLiteral tableLiteral) { - if (tableLiteral.values.Count == 0) - return [new EmptyArrayType()]; - - var arrayValues = new List(); - var keyedFields = new List(); - + var fields = new Dictionary(); foreach (var value in tableLiteral.values) { - switch (value) + var name = value.index; + var type = AnalyzeExpression(value.value).FirstOrDefault(); + + if (type == null) { - case ArrayField field: - arrayValues.Add(field.value); - break; - case StringIndexedField or ValueIndexedField: - keyedFields.Add(value); - break; - default: - throw new SwitchExpressionException(value); + _reporter.Report(new Diagnostic(TypeErrors.FailedTypeResolution, "Could not determine type of value", DiagnosticSeverity.Error, value.value.source)); + continue; } + + if (fields.TryAdd(name, type)) + continue; + + // TODO: Error code for duplicate fields in table + _reporter.Report(new Diagnostic("0", $"Duplicate field '{name}' in table literal", DiagnosticSeverity.Error, value.source)); } - if (arrayValues.Count > 0 && keyedFields.Count > 0) + return [new TableType(fields, TypeRegistry.AnyType)]; + } + + private List AnalyzeArrayLiteral(ArrayLiteral arrayLiteral) + { + if (arrayLiteral.values.Count == 0) + return [new EmptyArrayType()]; + + Type? common = null; + foreach (var value in arrayLiteral.values) { - _reporter.Report(new Diagnostic(TypeErrors.FailedTypeInference, "Cannot mix array values and keyed fields in a table literal", DiagnosticSeverity.Error, tableLiteral.source)); - return [TypeRegistry.UnknownType]; - } + var type = AnalyzeExpression(value).FirstOrDefault() ?? TypeRegistry.NilType; + common = common == null ? type : Type.GetCommonType(common, type); - if (keyedFields.Count > 0) - // TODO: Implement table type - return [TypeRegistry.UnknownType]; + if (common == null) + { + _reporter.Report(new Diagnostic(TypeErrors.FailedTypeInference, "Could not find a common type for array literal", DiagnosticSeverity.Error, arrayLiteral.source)); + return [TypeRegistry.UnknownType]; + } + } - return AnalyzeArrayValues(arrayValues, tableLiteral.source); + return [new ArrayType(common!, TypeRegistry.AnyType)]; } private List AnalyzeArrayValues(List values, Source source) diff --git a/Crater/SemanticAnalysis/Types/TableType.cs b/Crater/SemanticAnalysis/Types/TableType.cs new file mode 100644 index 0000000..bdf96f8 --- /dev/null +++ b/Crater/SemanticAnalysis/Types/TableType.cs @@ -0,0 +1,34 @@ +namespace Crater.SemanticAnalysis.Types; + +public class TableType(IReadOnlyDictionary fields, Type baseType) : Type("table", baseType) +{ + public readonly IReadOnlyDictionary Fields = fields; + + public override bool CanHold(Type other) + { + if (other is not TableType) + return false; + + return IsSameType(other); + } + + public override bool IsSameType(Type other) + { + if (other is not TableType otherTable) + return false; + + if (Fields.Count != otherTable.Fields.Count) + return false; + + foreach (var (key, type) in Fields) + { + if (!otherTable.Fields.TryGetValue(key, out var fieldType)) + return false; + + if (!type.IsSameType(fieldType)) + return false; + } + + return true; + } +} diff --git a/Crater/SyntaxTree/Nodes.cs b/Crater/SyntaxTree/Nodes.cs index b53b5aa..c439088 100644 --- a/Crater/SyntaxTree/Nodes.cs +++ b/Crater/SyntaxTree/Nodes.cs @@ -46,6 +46,8 @@ public sealed record NullableTypeName(TypeName baseTypeName, Source source) : Ty public sealed record ArrayTypeName(TypeName baseTypeName, Source source) : TypeName(source); +public sealed record TableTypeName(List fields, Source source) : TypeName(source); + public abstract record Expression(Source source) : Node(source); public sealed record ParenthesizedExpression(Expression innerExpression, Source source) : Expression(source); @@ -68,12 +70,8 @@ public sealed record BooleanLiteral(string value, Source source) : Expression(so public sealed record TableLiteral(List values, Source source) : Expression(source); -public record TableValue(Source source) : Node(source); - -public sealed record StringIndexedField(string index, Expression value, Source source) : TableValue(source); - -public sealed record ValueIndexedField(Expression index, Expression value, Source source) : TableValue(source); +public sealed record ArrayLiteral(List values, Source source) : Expression(source); -public sealed record ArrayField(Expression value, Source source) : TableValue(source); +public sealed record TableValue(string index, Expression value, Source source) : Node(source); public sealed record NilLiteral(string value, Source source) : Expression(source); diff --git a/Crater/SyntaxTree/SyntaxTreeConverter.cs b/Crater/SyntaxTree/SyntaxTreeConverter.cs index 5306b74..795d981 100644 --- a/Crater/SyntaxTree/SyntaxTreeConverter.cs +++ b/Crater/SyntaxTree/SyntaxTreeConverter.cs @@ -255,11 +255,20 @@ public override object VisitTypeName(CraterParser.TypeNameContext context) throw new Exception($"Unsupported decorated typename '{context.GetText()}'"); } - public override object VisitPrimaryType(CraterParser.PrimaryTypeContext context) + public override object VisitNamedType(CraterParser.NamedTypeContext context) { return new NamedTypeName(context.IDENTIFIER().GetText(), Source.FromContext(context)); } + public override object VisitTableDefinition(CraterParser.TableDefinitionContext context) + { + var fields = new List(); + foreach (var variableDeclaratorContext in context.variableDeclarator()) + fields.Add(Get(variableDeclaratorContext)); + + return new TableTypeName(fields, Source.FromContext(context)); + } + public override object VisitExpressionList(CraterParser.ExpressionListContext context) { var expressions = new List(); @@ -355,6 +364,15 @@ public override object VisitTableLiteral(CraterParser.TableLiteralContext contex return new TableLiteral(values, Source.FromContext(context)); } + public override object VisitArrayLiteral(CraterParser.ArrayLiteralContext context) + { + if (context.expressionList() == null) + return new ArrayLiteral([], Source.FromContext(context)); + + var values = Get>(context.expressionList()); + return new ArrayLiteral(values, Source.FromContext(context)); + } + public override object VisitNilLiteral(CraterParser.NilLiteralContext context) { return new NilLiteral(context.GetText(), Source.FromContext(context)); @@ -390,26 +408,12 @@ public override object VisitTableValues(CraterParser.TableValuesContext context) return values; } - public override object VisitStringIndexedField(CraterParser.StringIndexedFieldContext context) + public override object VisitTableValue(CraterParser.TableValueContext context) { var index = context.IDENTIFIER().GetText(); var value = Get(context.expression()); - return new StringIndexedField(index, value, Source.FromContext(context)); - } - - public override object VisitValueIndexedField(CraterParser.ValueIndexedFieldContext context) - { - var index = Get(context.index); - var value = Get(context.value); - - return new ValueIndexedField(index, value, Source.FromContext(context)); - } - - public override object VisitArrayField(CraterParser.ArrayFieldContext context) - { - var value = Get(context.expression()); - return new ArrayField(value, Source.FromContext(context)); + return new TableValue(index, value, Source.FromContext(context)); } private Expression BuildPostfixExpression(CraterParser.PostfixExpressionContext context, Expression prefix) From d774680e50188589ee3b5d05381601c1e16bb34b Mon Sep 17 00:00:00 2001 From: Periapsises <94928308+Periapsises@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:49:28 -0400 Subject: [PATCH 3/4] Add member access --- Crater/Compilation/Compiler.cs | 9 +++++++++ Crater/CraterLexer.g4 | 1 + Crater/CraterParser.g4 | 3 +++ Crater/SemanticAnalysis/SemanticAnalyzer.cs | 13 +++++++++++++ Crater/SemanticAnalysis/Type.cs | 5 +++++ Crater/SemanticAnalysis/Types/TableType.cs | 5 +++++ Crater/SyntaxTree/Nodes.cs | 2 ++ Crater/SyntaxTree/SyntaxTreeConverter.cs | 9 +++++++++ 8 files changed, 47 insertions(+) diff --git a/Crater/Compilation/Compiler.cs b/Crater/Compilation/Compiler.cs index 77ca610..4228de9 100644 --- a/Crater/Compilation/Compiler.cs +++ b/Crater/Compilation/Compiler.cs @@ -339,6 +339,9 @@ private void CompileExpression(Expression expression) case FunctionCall functionCall: CompileFunctionCall(functionCall); break; + case DotIndexing dotIndexing: + CompileDotIndexing(dotIndexing); + break; default: throw new SwitchExpressionException(expression); } @@ -437,4 +440,10 @@ private void CompileFunctionCall(FunctionCall functionCall) _writer.Write(")"); } + + private void CompileDotIndexing(DotIndexing dotIndexing) + { + CompileExpression(dotIndexing.prefix); + _writer.Write($".{dotIndexing.key}"); + } } diff --git a/Crater/CraterLexer.g4 b/Crater/CraterLexer.g4 index cf0313d..b467d62 100644 --- a/Crater/CraterLexer.g4 +++ b/Crater/CraterLexer.g4 @@ -57,6 +57,7 @@ SLASH : '/'; LESSER : '<'; GREATER : '>'; +DOT : '.'; COMMA : ','; COLON : ':'; ASSIGN : '='; diff --git a/Crater/CraterParser.g4 b/Crater/CraterParser.g4 index 7c72442..6110899 100644 --- a/Crater/CraterParser.g4 +++ b/Crater/CraterParser.g4 @@ -97,12 +97,15 @@ prefixExpression postfixExpression : postfixFunctionCall | postfixBracketIndexing + | postfixDotIndexing ; postfixFunctionCall: LPAREN expressionList? RPAREN; postfixBracketIndexing: LSQRBRACKET expression RSQRBRACKET; +postfixDotIndexing: DOT IDENTIFIER; + tableValues: tableValue (COMMA tableValue)* COMMA?; tableValue: IDENTIFIER ASSIGN expression; diff --git a/Crater/SemanticAnalysis/SemanticAnalyzer.cs b/Crater/SemanticAnalysis/SemanticAnalyzer.cs index fe9a2b9..9b2fd6e 100644 --- a/Crater/SemanticAnalysis/SemanticAnalyzer.cs +++ b/Crater/SemanticAnalysis/SemanticAnalyzer.cs @@ -452,6 +452,7 @@ private List AnalyzeExpression(Expression expression) VariableReference variableReference => AnalyzeVariableReference(variableReference), FunctionCall functionCall => AnalyzeFunctionCall(functionCall), BracketIndexing bracketIndexing => AnalyzeBracketIndexing(bracketIndexing), + DotIndexing dotIndexing => AnalyzeDotIndexing(dotIndexing), _ => throw new SwitchExpressionException(expression) }; } @@ -659,6 +660,18 @@ private List AnalyzeBracketIndexing(BracketIndexing bracketIndexing) return [TypeRegistry.UnknownType]; } + private List AnalyzeDotIndexing(DotIndexing dotIndexing) + { + var prefixType = AnalyzeExpression(dotIndexing.prefix).FirstOrDefault() ?? TypeRegistry.NilType; + var resultType = prefixType.ResolveMemberAccess(dotIndexing.key); + if (resultType != null) + return [resultType]; + + // TODO: Error code for member access not supported + _reporter.Report(new Diagnostic("0", $"Cannot perform member access on '{prefixType}'", DiagnosticSeverity.Error, dotIndexing.source)); + return [TypeRegistry.UnknownType]; + } + private bool IsTernaryPattern(BinaryOperation binaryOperation, [NotNullWhen(true)] out Expression? valueA, [NotNullWhen(true)] out Expression? valueB) { valueA = null; diff --git a/Crater/SemanticAnalysis/Type.cs b/Crater/SemanticAnalysis/Type.cs index 487ad19..165aa46 100644 --- a/Crater/SemanticAnalysis/Type.cs +++ b/Crater/SemanticAnalysis/Type.cs @@ -114,5 +114,10 @@ public virtual bool IsSameType(Type other) return null; } + public virtual Type? ResolveMemberAccess(string key) + { + return null; + } + public override string ToString() => GetName(); } diff --git a/Crater/SemanticAnalysis/Types/TableType.cs b/Crater/SemanticAnalysis/Types/TableType.cs index bdf96f8..13fc14f 100644 --- a/Crater/SemanticAnalysis/Types/TableType.cs +++ b/Crater/SemanticAnalysis/Types/TableType.cs @@ -31,4 +31,9 @@ public override bool IsSameType(Type other) return true; } + + public override Type? ResolveMemberAccess(string key) + { + return Fields.GetValueOrDefault(key); + } } diff --git a/Crater/SyntaxTree/Nodes.cs b/Crater/SyntaxTree/Nodes.cs index c439088..df08538 100644 --- a/Crater/SyntaxTree/Nodes.cs +++ b/Crater/SyntaxTree/Nodes.cs @@ -58,6 +58,8 @@ public sealed record FunctionCall(Expression function, List argument public sealed record BracketIndexing(Expression prefix, Expression index, Source source) : Expression(source); +public sealed record DotIndexing(Expression prefix, string key, Source source) : Expression(source); + public sealed record UnaryOperation(string op, Expression expression, Source source) : Expression(source); public sealed record BinaryOperation(Expression left, string op, Expression right, Source source) : Expression(source); diff --git a/Crater/SyntaxTree/SyntaxTreeConverter.cs b/Crater/SyntaxTree/SyntaxTreeConverter.cs index 795d981..7d5634b 100644 --- a/Crater/SyntaxTree/SyntaxTreeConverter.cs +++ b/Crater/SyntaxTree/SyntaxTreeConverter.cs @@ -424,6 +424,9 @@ private Expression BuildPostfixExpression(CraterParser.PostfixExpressionContext if (context.postfixBracketIndexing() is { } postfixBracketIndexingContext) return BuildPostfixBracketIndexing(postfixBracketIndexingContext, prefix); + if (context.postfixDotIndexing() is { } postfixDotIndexingContext) + return BuildPostfixDotIndexing(postfixDotIndexingContext, prefix); + throw new InvalidOperationException($"Unsupported postfix expression type: {context.GetText()}"); } @@ -441,4 +444,10 @@ private BracketIndexing BuildPostfixBracketIndexing(CraterParser.PostfixBracketI var index = Get(context.expression()); return new BracketIndexing(prefix, index, Source.FromContext(context)); } + + private DotIndexing BuildPostfixDotIndexing(CraterParser.PostfixDotIndexingContext context, Expression prefix) + { + var key = context.IDENTIFIER().GetText(); + return new DotIndexing(prefix, key, Source.FromContext(context)); + } } From 1f07f32185b1a0e0dfabbf9735393ec6750bc22a Mon Sep 17 00:00:00 2001 From: Periapsises <94928308+Periapsises@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:51:36 -0400 Subject: [PATCH 4/4] Fix missing compile method for bracket indexing --- Crater/Compilation/Compiler.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Crater/Compilation/Compiler.cs b/Crater/Compilation/Compiler.cs index 4228de9..2fcd5f9 100644 --- a/Crater/Compilation/Compiler.cs +++ b/Crater/Compilation/Compiler.cs @@ -339,6 +339,9 @@ private void CompileExpression(Expression expression) case FunctionCall functionCall: CompileFunctionCall(functionCall); break; + case BracketIndexing bracketIndexing: + CompileBracketIndexing(bracketIndexing); + break; case DotIndexing dotIndexing: CompileDotIndexing(dotIndexing); break; @@ -441,6 +444,14 @@ private void CompileFunctionCall(FunctionCall functionCall) _writer.Write(")"); } + private void CompileBracketIndexing(BracketIndexing bracketIndexing) + { + CompileExpression(bracketIndexing.prefix); + _writer.Write("["); + CompileExpression(bracketIndexing.index); + _writer.Write("]"); + } + private void CompileDotIndexing(DotIndexing dotIndexing) { CompileExpression(dotIndexing.prefix);