diff --git a/FadeBasic/CHANGELOG.md b/FadeBasic/CHANGELOG.md index fe878e9..29fae99 100644 --- a/FadeBasic/CHANGELOG.md +++ b/FadeBasic/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.38] - 2025-03-07 + +### Added +- The `default` keyword +- Object initializer pattern + ## [0.0.37] - 2025-03-02 ### Changed diff --git a/FadeBasic/FadeBasic/Ast/ExpressionNode.cs b/FadeBasic/FadeBasic/Ast/ExpressionNode.cs index 215dd58..c171865 100644 --- a/FadeBasic/FadeBasic/Ast/ExpressionNode.cs +++ b/FadeBasic/FadeBasic/Ast/ExpressionNode.cs @@ -278,6 +278,19 @@ public override IEnumerable IterateChildNodes() } } + public class DefaultValueExpression : AstNode, ILiteralNode + { + protected override string GetString() + { + return "default"; + } + + public override IEnumerable IterateChildNodes() + { + yield break; + } + } + public class LiteralIntExpression : AstNode, ILiteralNode { public int value; diff --git a/FadeBasic/FadeBasic/Ast/InitializerExpression.cs b/FadeBasic/FadeBasic/Ast/InitializerExpression.cs new file mode 100644 index 0000000..99b1cb1 --- /dev/null +++ b/FadeBasic/FadeBasic/Ast/InitializerExpression.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FadeBasic.Ast +{ + public class InitializerExpression : AstNode, IExpressionNode + { + public List assignments = new List(); + + + protected override string GetString() + { + return $"init ({string.Join(",", assignments.Select(x => x.ToString()))})"; + } + + public override IEnumerable IterateChildNodes() + { + foreach (var x in assignments) + yield return x; + } + } +} \ No newline at end of file diff --git a/FadeBasic/FadeBasic/Ast/Visitors/InitializerSugarVisitor.cs b/FadeBasic/FadeBasic/Ast/Visitors/InitializerSugarVisitor.cs new file mode 100644 index 0000000..15e6732 --- /dev/null +++ b/FadeBasic/FadeBasic/Ast/Visitors/InitializerSugarVisitor.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; + +namespace FadeBasic.Ast.Visitors +{ + public static class InitializerSugarVisitor + { + + static void ApplyStatements(List statements) + { + for (var i = 0; i < statements.Count; i++) + { + var statement = statements[i]; + + switch (statement) + { + case DeclarationStatement decl: + ApplyDecl(decl, i, statements); + break; + case AssignmentStatement assignment: + ApplyAssign(assignment, i, statements); + break; + } + + } + } + + static (IVariableNode outputLeft, IVariableNode outputRight) ReBalance(IVariableNode left, IVariableNode right) + { + switch (left) + { + case StructFieldReference leftStructRef: + + var subLeft = leftStructRef.left; + var subRight = leftStructRef.right; + + var (balancedLeft, balancedRight) = ReBalance(subLeft, subRight); + + var newRight = new StructFieldReference + { + startToken = subRight.StartToken, + endToken = right.EndToken, + left = balancedRight, + right = right + }; + return (balancedLeft, newRight); + + break; + default: + return (left, right); + } + } + + static void ApplyAssign(AssignmentStatement assignment, int index, List statements) + { + if (!(assignment.expression is InitializerExpression init)) return; + + assignment.expression = new DefaultValueExpression + { + startToken = assignment.startToken, + endToken = assignment.endToken + }; + + for (var i = init.assignments.Count - 1; i >= 0; i--) + { + var subAssignment = init.assignments[i]; + + + // need to re-balance. if left is already a struct-field-reference, then must dig in. + + var (left, right) = ReBalance(assignment.variable, subAssignment.variable); + + subAssignment.variable = new StructFieldReference + { + startToken = subAssignment.startToken, + endToken = subAssignment.endToken, + Errors = subAssignment.Errors, + + // TODO: this probably isn't right? + // left = new VariableRefNode(assignment.startToken, subAssignment.variable), + // right = assignment.variable + left = left, + right = right + }; + statements.Insert(index + 1, subAssignment); + } + } + + static void ApplyDecl(DeclarationStatement decl, int index, List statements) + { + if (!(decl.initializerExpression is InitializerExpression init)) return; + decl.initializerExpression = null; + for (var i = init.assignments.Count - 1; i >= 0; i--) + { + var assignment = init.assignments[i]; + assignment.variable = new StructFieldReference + { + startToken = assignment.startToken, + endToken = assignment.endToken, + Errors = assignment.Errors, + + // TODO: this probably isn't right? + left = new VariableRefNode(decl.startToken, decl.variable), + right = assignment.variable + }; + statements.Insert(index + 1, assignment); + } + } + + public static void AddInitializerSugar(this ProgramNode node) + { + ApplyStatements(node.statements); + } + } +} \ No newline at end of file diff --git a/FadeBasic/FadeBasic/Ast/Visitors/ScopeErrorVisitor.cs b/FadeBasic/FadeBasic/Ast/Visitors/ScopeErrorVisitor.cs index 074c2a3..e4fc8fb 100644 --- a/FadeBasic/FadeBasic/Ast/Visitors/ScopeErrorVisitor.cs +++ b/FadeBasic/FadeBasic/Ast/Visitors/ScopeErrorVisitor.cs @@ -95,6 +95,14 @@ public static void AddScopeRelatedErrors(this ProgramNode program, ParseOptions } + foreach (var def in scope.defaultValueExpressions) + { + if (def.ParsedType.type == VariableType.Void) + { + def.Errors.Add(new ParseError(def, ErrorCodes.DefaultExpressionUnknownType)); + } + } + scope.DoDelayedTypeChecks(); } @@ -230,6 +238,11 @@ static void CheckStatements(this List statements, Scope scope, E if (decl.initializerExpression != null) { + if (decl.initializerExpression is DefaultValueExpression defExpr) + { + defExpr.ParsedType = decl.ParsedType; + } + scope.EnforceTypeAssignment(decl.initializerExpression, decl.initializerExpression.ParsedType, decl.ParsedType, false, out _); } @@ -242,6 +255,7 @@ static void CheckStatements(this List statements, Scope scope, E // and THEN register LHS of the assignemnt (otherwise you can get self-referential stuff) scope.AddAssignment(assignment, ctx, out var implicitDecl); + if (implicitDecl != null) { statements.Insert(i, implicitDecl); @@ -257,6 +271,11 @@ static void CheckStatements(this List statements, Scope scope, E default: break; } + + if (assignment.expression is DefaultValueExpression defExpr2 && assignment.variable.ParsedType.type != VariableType.Void) + { + defExpr2.ParsedType = assignment.variable.ParsedType; + } break; case SwitchStatement switchStatement: @@ -501,6 +520,13 @@ public static void EnsureVariablesAreDefined(this IExpressionNode expr, Scope sc { switch (expr) { + case DefaultValueExpression defExpr: + scope.AddDefaultExpression(defExpr); + break; + case InitializerExpression initExpr: + // initializers are not allowed to appear here; they are syntax sugar and should be removed by now. + initExpr.Errors.Add(new ParseError(initExpr.startToken, ErrorCodes.InitializerNotAllowed)); + break; case BinaryOperandExpression binaryOpExpr: binaryOpExpr.lhs.EnsureVariablesAreDefined(scope, ctx); binaryOpExpr.rhs.EnsureVariablesAreDefined(scope, ctx); diff --git a/FadeBasic/FadeBasic/Errors.cs b/FadeBasic/FadeBasic/Errors.cs index c374f9e..506e07e 100644 --- a/FadeBasic/FadeBasic/Errors.cs +++ b/FadeBasic/FadeBasic/Errors.cs @@ -194,6 +194,8 @@ public static class ErrorCodes public static readonly ErrorCode UnknownType = "[0210] Type is not defined"; public static readonly ErrorCode ArrayRankMustBeInteger = "[0211] Array rank expression must be an integer"; public static readonly ErrorCode ImplicitArrayDeclaration = "[0212] Implicit array declarations are not allowed"; + public static readonly ErrorCode InitializerNotAllowed = "[0213] Initializer is not allowed here"; + public static readonly ErrorCode DefaultExpressionUnknownType = "[0214] Default expression has unknown type"; // 300 series represents type issues public static readonly ErrorCode SymbolAlreadyDeclared = "[0300] Symbol already declared"; diff --git a/FadeBasic/FadeBasic/Lexer.cs b/FadeBasic/FadeBasic/Lexer.cs index 744b310..3196ae0 100644 --- a/FadeBasic/FadeBasic/Lexer.cs +++ b/FadeBasic/FadeBasic/Lexer.cs @@ -101,6 +101,8 @@ public enum LexemType OpBitwiseXor, ParenOpen, ParenClose, + BracketOpen, + BracketClose, LiteralReal, LiteralInt, LiteralString, @@ -154,6 +156,8 @@ public class Lexer new Lexem(-10,LexemType.WhiteSpace, new Regex("^(\\s|\\t|\\n)+")), new Lexem(LexemType.ParenOpen, new Regex("^\\(")), new Lexem(LexemType.ParenClose, new Regex("^\\)")), + new Lexem(LexemType.BracketOpen, new Regex("^\\{")), + new Lexem(LexemType.BracketClose, new Regex("^\\}")), new Lexem(LexemType.OpPlus, new Regex("^\\+")), new Lexem(LexemType.OpMinus, new Regex("^\\-")), new Lexem(LexemType.OpMultiply, new Regex("^\\*")), diff --git a/FadeBasic/FadeBasic/Parser.cs b/FadeBasic/FadeBasic/Parser.cs index 793c86b..037c8c6 100644 --- a/FadeBasic/FadeBasic/Parser.cs +++ b/FadeBasic/FadeBasic/Parser.cs @@ -74,7 +74,7 @@ public class Scope public Dictionary functionSymbolTable = new Dictionary(); public Dictionary functionTable = new Dictionary(); public Dictionary> functionReturnTypeTable = new Dictionary>(); - + public List defaultValueExpressions = new List(); List delayedTypeChecks = new List(); private int allowExitCounter; @@ -397,6 +397,11 @@ public void AddAssignment(AssignmentStatement assignment, EnsureTypeContext ctx, // declr is optional... if (TryGetSymbol(variableRef.variableName, out var existingSymbol)) { + if (assignment.expression is DefaultValueExpression defExpr) + { + defExpr.ParsedType = existingSymbol.typeInfo; + } + EnforceTypeAssignment(variableRef, assignment.expression.ParsedType, existingSymbol.typeInfo, false, out _); variableRef.DeclaredFromSymbol = existingSymbol; } @@ -409,7 +414,17 @@ public void AddAssignment(AssignmentStatement assignment, EnsureTypeContext ctx, }; var rightType = assignment.expression.ParsedType; - EnforceTypeAssignment(variableRef, rightType, defaultTypeInfo, true, out var foundType); + TypeInfo foundType = default; + if (assignment.expression is DefaultValueExpression defExpr) + { + defExpr.ParsedType = defaultTypeInfo; + variableRef.ParsedType = defaultTypeInfo; + foundType = defaultTypeInfo; + } + else + { + EnforceTypeAssignment(variableRef, rightType, defaultTypeInfo, true, out foundType); + } var locals = GetVariables(DeclarationScopeType.Local); @@ -456,7 +471,14 @@ public void AddAssignment(AssignmentStatement assignment, EnsureTypeContext ctx, } } - EnforceTypeAssignment(indexRef, assignment.expression.ParsedType, nonArrayVersion, false, out _); + if (assignment.expression is DefaultValueExpression defExpr) + { + defExpr.ParsedType = nonArrayVersion; + } + else + { + EnforceTypeAssignment(indexRef, assignment.expression.ParsedType, nonArrayVersion, false, out _); + } indexRef.DeclaredFromSymbol = existingArrSymbol; } // EnforceTypeAssignment(variableRef, rightType, defaultTypeInfo, true, out var foundType); @@ -806,6 +828,11 @@ class DelayedTypeCheck { public IAstNode source, right, left; } + + public void AddDefaultExpression(DefaultValueExpression defExpr) + { + defaultValueExpressions.Add(defExpr); + } } public class ParseOptions @@ -867,6 +894,7 @@ public ProgramNode ParseProgram(ParseOptions options = null) program.endToken = _stream.Current; // program.AddTypeInfo(); + program.AddInitializerSugar(); program.AddScopeRelatedErrors(options); return program; @@ -2970,7 +2998,60 @@ private bool TryParseWikiTerm(out IExpressionNode outputExpression, out ProgramR recovery = null; switch (token.type) { - + case LexemType.KeywordCaseDefault: + _stream.Advance(); + outputExpression = new DefaultValueExpression + { + startToken = token, endToken = token + }; + break; + case LexemType.BracketOpen: + _stream.Advance(); // consume open bracket + outputExpression = null; + + var lookingForClose = true; + var subStatements = new List(); + var assignments = new List(); + while (lookingForClose) + { + switch (_stream.Peek.type) + { + case LexemType.EOF: + // TODO: add error + // errors.Add(new ParseError(start, ErrorCodes.TypeDefMissingEndType)); + lookingForClose = false; + break; + + case LexemType.EndStatement: + _stream.Advance(); + break; + case LexemType.BracketClose: + lookingForClose = false; + _stream.Advance(); + break; + default: + var statement = ParseStatement(subStatements); + subStatements.Add(statement); + + switch (statement) + { + case AssignmentStatement assignment: + assignments.Add(assignment); + break; + default: + // TODO: add error saying only assignments are allowed + break; + } + break; + } + + } + + outputExpression = new InitializerExpression + { + startToken = token, endToken = _stream.Current, assignments = assignments + }; + break; case LexemType.CommandWord: _stream.Advance(); diff --git a/FadeBasic/FadeBasic/Virtual/Compiler.cs b/FadeBasic/FadeBasic/Virtual/Compiler.cs index 218e4d3..fb13be4 100644 --- a/FadeBasic/FadeBasic/Virtual/Compiler.cs +++ b/FadeBasic/FadeBasic/Virtual/Compiler.cs @@ -2221,6 +2221,34 @@ public void Compile(IExpressionNode expr) // CompiledVariable compiledVar = null; switch (expr) { + case DefaultValueExpression defExpr: + // the default value for any type is just zeros, right? + + switch (defExpr.ParsedType.type) + { + case VariableType.String: + Compile(new LiteralStringExpression(defExpr.startToken, "")); + break; + case VariableType.Struct: + + if (_types.TryGetValue(defExpr.ParsedType.structName, out var typeInfo)) + { + AddPushZeros(_buffer, TypeCodes.STRUCT, typeInfo.byteSize); + } + else + { + throw new Exception("unknown type reference" + defExpr.ParsedType.structName); + } + break; + default: + // push the type-code for this def-expr + var tc = VmUtil.GetTypeCode(defExpr.ParsedType.type); + + // everything else is an empty zero block + AddPushZeros(_buffer, tc, TypeCodes.GetByteSize(tc)); + break; + } + break; case CommandExpression commandExpr: Compile(new CommandStatement { @@ -2680,6 +2708,18 @@ private static void AddPushInt(List buffer, int x) } } + + private static void AddPushZeros(List buffer, byte typeCode, int howManyBytesOfZero) + { + buffer.Add(OpCodes.PUSH_ZEROS); + buffer.Add(typeCode); + var value = BitConverter.GetBytes(howManyBytesOfZero); + for (var i = 0; i < value.Length; i++) + { + buffer.Add(value[i]); + } + } + private static void AddPushULongNoTypeCode(List buffer, ulong x) { var value = BitConverter.GetBytes(x); diff --git a/FadeBasic/FadeBasic/Virtual/FastStack.cs b/FadeBasic/FadeBasic/Virtual/FastStack.cs index 6bb37d1..6467b0c 100644 --- a/FadeBasic/FadeBasic/Virtual/FastStack.cs +++ b/FadeBasic/FadeBasic/Virtual/FastStack.cs @@ -69,6 +69,15 @@ void Expand(int wiggle) } } + public void PushFiller(T filler, int length) + { + Expand(length); + + for (var n = 0; n < length; n ++) + { + buffer[ptr++] = filler; + } + } public void PushSpan(ReadOnlySpan data, int length) { diff --git a/FadeBasic/FadeBasic/Virtual/OpCodes.cs b/FadeBasic/FadeBasic/Virtual/OpCodes.cs index 0263459..0ddef0f 100644 --- a/FadeBasic/FadeBasic/Virtual/OpCodes.cs +++ b/FadeBasic/FadeBasic/Virtual/OpCodes.cs @@ -251,6 +251,12 @@ public static class OpCodes /// Exactly the same as push, but it does not push the type code aftewards /// public const byte PUSH_TYPELESS = 18; + + /// + /// The next INS should be a number, and then this will inject that number of ZEROs + /// onto the program stack + /// + public const byte PUSH_ZEROS = 59; /// /// Pushes a type-format byte array from INS onto the stack. diff --git a/FadeBasic/FadeBasic/Virtual/VirtualMachine.cs b/FadeBasic/FadeBasic/Virtual/VirtualMachine.cs index 85c39f8..f7cdfe8 100644 --- a/FadeBasic/FadeBasic/Virtual/VirtualMachine.cs +++ b/FadeBasic/FadeBasic/Virtual/VirtualMachine.cs @@ -376,6 +376,16 @@ public void Execute2(int instructionBatchCount=1000) var code = Advance(); stack.Push(code); break; + case OpCodes.PUSH_ZEROS: + // next 4 bytes in INS are zero-amount + typeCode = Advance(); + var amount = BitConverter.ToInt32(program, instructionIndex); + // var amountSpan = program.AsSpan(instructionIndex, 4); + instructionIndex += sizeof(int); + stack.PushFiller(0, amount); + stack.Push(typeCode); + + break; case OpCodes.PUSH_TYPELESS: typeCode = Advance(); diff --git a/FadeBasic/LSP/Handlers/SemanticTokenHandler.cs b/FadeBasic/LSP/Handlers/SemanticTokenHandler.cs index 73e537f..b92b1f2 100644 --- a/FadeBasic/LSP/Handlers/SemanticTokenHandler.cs +++ b/FadeBasic/LSP/Handlers/SemanticTokenHandler.cs @@ -208,6 +208,9 @@ static SemanticTokenType ConvertSymbol(LexemType lexem) case LexemType.KeywordTypeDWord: return SemanticTokenType.Type; + + case LexemType.BracketClose: + case LexemType.BracketOpen: case LexemType.ParenClose: case LexemType.ParenOpen: case LexemType.OpPlus: diff --git a/FadeBasic/Tests/ParserTests.cs b/FadeBasic/Tests/ParserTests.cs index 259ead6..ee11645 100644 --- a/FadeBasic/Tests/ParserTests.cs +++ b/FadeBasic/Tests/ParserTests.cs @@ -695,11 +695,67 @@ public void AnasUnfunTest() var code = prog.ToString(); Assert.That(code, Is.EqualTo("((= (ref x),(?> (+ (1),(2)),(3))))")); } + + + [Test] + public void Default_int() + { + var input = @" + +x = default ` reset the object +"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + Assert.That(prog.statements.Count, Is.EqualTo(1)); + var code = prog.ToString(); + Assert.That(code, Is.EqualTo("((= (ref x),(default)))")); + } + + [Test] + public void Default_Type() + { + var input = @" +type egg + x, y +endtype + +e as egg = default ` reset the object +"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + Assert.That(prog.statements.Count, Is.EqualTo(1)); + var code = prog.ToString(); + Assert.That(code, Is.EqualTo("((type egg ((ref x) as (integer)),((ref y) as (integer))),(decl local,e,(typeRef egg),(default)))")); + } [Test] - public void Doop() + public void Initializers_Test() { - var x = (1 * 3) > 3; + var input = @" +type egg + x, y +endtype + +e as egg = { + x = 1 + y = 2 +} +"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + Assert.That(prog.statements.Count, Is.EqualTo(3)); + var code = prog.ToString(); + Assert.That(code, Is.EqualTo(@"( +(type egg +((ref x) as (integer)), +((ref y) as (integer)) +), +(decl local,e,(typeRef egg)), +(= ((ref e).(ref x)),(1)), +(= ((ref e).(ref y)),(2)))".ReplaceLineEndings("").Replace("\t", ""))); } [Test] @@ -1186,6 +1242,36 @@ y as hotdog (= ((ref y).(ref x)),(2)) )".ReplaceLineEndings(""))); } + + +// [Test] +// public void Type_Assignment_Nested() +// { +// var input = @" +// type hotdog +// x +// endtype +// type food +// h as hotdog +// endtype +// type cave +// f as food +// endtype +// y as cave +// y.f.h.x = 2 +// "; +// var parser = MakeParser(input); +// var prog = parser.ParseProgram(); +// prog.AssertNoParseErrors(); +// +// var code = prog.ToString(); +// Console.WriteLine(code); +// Assert.That(code, Is.EqualTo(@"( +// (type hotdog ((ref x) as (integer))), +// (decl local,y,(typeRef hotdog)), +// (= ((ref y).(ref x)),(2)) +// )".ReplaceLineEndings(""))); +// } [Test] diff --git a/FadeBasic/Tests/ParserTests_Erros.cs b/FadeBasic/Tests/ParserTests_Erros.cs index c2ddda3..f5840f6 100644 --- a/FadeBasic/Tests/ParserTests_Erros.cs +++ b/FadeBasic/Tests/ParserTests_Erros.cs @@ -292,6 +292,171 @@ public void ParseError_TypeCheck_IntToFloat() } + [Test] + public void ParseError_TypeCheck_Default_String() + { + var input = @" +x$ = default +"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + } + + [Test] + public void ParseError_TypeCheck_Default_Array_Element_Int_Okay() + { + var input = @" +DIM x(3) +x(1) = default +"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + } + + + [Test] + public void ParseError_TypeCheck_Default_Array_Element_Type_Okay() + { + var input = @" +TYPE egg + x, y +ENDTYPE +DIM x(3) as egg +x(1) = default +"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + } + + + [Test] + public void ParseError_TypeCheck_Default_Decl_TypeOkay() + { + var input = @" +type egg + x, y +endtype +e as egg = default"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + } + + + [Test] + public void ParseError_TypeCheck_Default_Assign_TypeOkay() + { + var input = @" +type egg + x, y +endtype +e as egg + +e = default +"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + } + + + [Test] + public void ParseError_TypeCheck_Default_Assign_IntOkay() + { + var input = @" +e = default +"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + } + + + [TestCase("{}")] + [TestCase(@"{ + x = 2 +}")] + [TestCase(@"{ + x = 2 + y = 1 +}")] + public void ParseError_TypeCheck_Init_Okay(string snippets) + { + var input = @" +type egg + x, y +endtype + +e as egg = " + snippets + "\n"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + } + + + [TestCase("{}")] + [TestCase(@"{ + a = 3 +}")] + [TestCase(@"{ + nest.x = 1 +}")] + [TestCase(@"{ + nest = { + x = 1 + } +}")] + public void ParseError_TypeCheck_Init_NestedOkay(string snippets) + { + var input = @" +type egg + x, y +endtype +type chicken + a + nest as egg +endtype + +c as chicken = " + snippets + "\n"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertNoParseErrors(); + } + + + + [Test] + public void ParseError_TypeCheck_DefaultNotOkayInNonLiterals() + { + var input = @" +x = default + 3"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertParseErrors(1, out var errors); + Assert.That(errors[0].Display, Is.EqualTo($"[1:4] - {ErrorCodes.DefaultExpressionUnknownType}")); + } + + + [Test] + public void ParseError_TypeCheck_Init_AssignmentOkay() + { + var input = @" +type egg + x, y +endtype +e as egg +e = { +}"; + var parser = MakeParser(input); + var prog = parser.ParseProgram(); + prog.AssertParseErrors(0, out var errors); + // Assert.That(errors[0].Display, Is.EqualTo($"[1:4] - {ErrorCodes.InitializerNotAllowed}")); + } + + [Test] public void ParseError_TypeCheck_Function_ReturnTypeAmbig() { diff --git a/FadeBasic/Tests/TokenVm.cs b/FadeBasic/Tests/TokenVm.cs index b012216..0dfa752 100644 --- a/FadeBasic/Tests/TokenVm.cs +++ b/FadeBasic/Tests/TokenVm.cs @@ -2958,6 +2958,174 @@ y as egg } + [TestCase("")] + [TestCase("y = 123")] + public void Default_Int_Assign(string snippet) + { + var src = @$" +{snippet} +y = default +"; + Setup(src, out var compiler, out var prog); + var vm = new VirtualMachine(prog); + vm.hostMethods = compiler.methodTable; + vm.Execute2(); + + Assert.That(VmUtil.ConvertToInt(vm.dataRegisters[0]), Is.EqualTo(0)); + } + + + [TestCase("x$ = default")] + [TestCase("x$ = \"\"")] + public void Default_String_Assign(string src) + { + Setup(src, out var compiler, out var prog); + var vm = new VirtualMachine(prog); + vm.hostMethods = compiler.methodTable; + vm.Execute2(); + + + Assert.That(vm.dataRegisters[0], Is.EqualTo(0)); // the ptr to the string in memory + Assert.That(vm.typeRegisters[0], Is.EqualTo(TypeCodes.STRING)); + + vm.heap.Read(0.ToPtr(), 0, out var memory); + var str = VmConverter.ToString(memory); + + // Assert.That(str, Is.EqualTo("hello")); + // Assert.That(VmUtil.ConvertToInt(vm.dataRegisters[0]), Is.EqualTo(0)); + } + + + + [TestCase("")] + [TestCase("y# = 2.0")] + [TestCase("y# = 123")] + public void Default_Float_Assign(string snippet) + { + var src = @$" +{snippet} +y# = default +"; + Setup(src, out var compiler, out var prog); + var vm = new VirtualMachine(prog); + vm.hostMethods = compiler.methodTable; + vm.Execute2(); + + Assert.That(VmUtil.ConvertToFloat(vm.dataRegisters[0]), Is.EqualTo(0)); + } + + [Test] + public void Type_Default_Decl() + { + var src = @" +type egg +x +endtype + +y as egg = default +y.x = 31 +y = default +"; + Setup(src, out var compiler, out var prog); + var vm = new VirtualMachine(prog); + vm.hostMethods = compiler.methodTable; + vm.Execute2(); + + Assert.That(vm.heap.Cursor, Is.EqualTo(4.ToPtr())); // size of the only field in egg, int, 4. + Assert.That(vm.typeRegisters[0], Is.EqualTo(TypeCodes.STRUCT)); + + vm.heap.Read(vm.dataRegisters[0].ToPtr(), 4, out var memory); + var data = BitConverter.ToInt32(memory); + Assert.That(data, Is.EqualTo(0)); + } + + + [Test] + public void Type_Init_Decl() + { + var src = $@" +type egg +x +endtype + +y as egg = {{ + x = 43 +}} +"; + Setup(src, out var compiler, out var prog); + var vm = new VirtualMachine(prog); + vm.hostMethods = compiler.methodTable; + vm.Execute2(); + + Assert.That(vm.heap.Cursor, Is.EqualTo(4.ToPtr())); // size of the only field in egg, int, 4. + Assert.That(vm.typeRegisters[0], Is.EqualTo(TypeCodes.STRUCT)); + + vm.heap.Read(vm.dataRegisters[0].ToPtr(), 4, out var memory); + var data = BitConverter.ToInt32(memory); + Assert.That(data, Is.EqualTo(43)); + } + + + [Test] + public void Type_Init_Assign_Blank() + { + var src = $@" +type egg +x +endtype + +y as egg = {{ + x = 43 +}} + +y = {{ + ` sugar for y = default +}} +"; + Setup(src, out var compiler, out var prog); + var vm = new VirtualMachine(prog); + vm.hostMethods = compiler.methodTable; + vm.Execute2(); + + Assert.That(vm.heap.Cursor, Is.EqualTo(4.ToPtr())); // size of the only field in egg, int, 4. + Assert.That(vm.typeRegisters[0], Is.EqualTo(TypeCodes.STRUCT)); + + vm.heap.Read(vm.dataRegisters[0].ToPtr(), 4, out var memory); + var data = BitConverter.ToInt32(memory); + Assert.That(data, Is.EqualTo(0)); + } + + + [Test] + public void Type_Init_Assign_Reset() + { + var src = $@" +type egg +x, y +endtype + +y as egg = {{ + x = 43 +}} + +y = {{ + y = 12 +}} +"; + Setup(src, out var compiler, out var prog); + var vm = new VirtualMachine(prog); + vm.hostMethods = compiler.methodTable; + vm.Execute2(); + + Assert.That(vm.heap.Cursor, Is.EqualTo(8.ToPtr())); // size of the 2 fields in egg, int, 4., times 2 + Assert.That(vm.typeRegisters[0], Is.EqualTo(TypeCodes.STRUCT)); + + vm.heap.Read(vm.dataRegisters[0].ToPtr() + 4, 4, out var memory); + var data = BitConverter.ToInt32(memory); + Assert.That(data, Is.EqualTo(12)); + } + + [Test] public void Type_Instantiate_Assign() { diff --git a/FadeBasic/book/FadeBook/Dark Basic Pro Changes.md b/FadeBasic/book/FadeBook/Dark Basic Pro Changes.md index 067fa05..4358558 100644 --- a/FadeBasic/book/FadeBook/Dark Basic Pro Changes.md +++ b/FadeBasic/book/FadeBook/Dark Basic Pro Changes.md @@ -78,6 +78,10 @@ FUNCTION getEgg() ENDFUNCTION egg ``` +## UDT Initializers + +_Fade_ supports [object initializers](https://github.com/cdhanna/fadebasic/blob/main/FadeBasic/book/FadeBook/Language.md#udt-initializer), where-as _Dark Basic Pro_ did not. + ## No String Concatenation with semicolons _Fade Basic_ does not support string concatenation with the `;` character. Instead, please use the `+` character. diff --git a/FadeBasic/book/FadeBook/Language.md b/FadeBasic/book/FadeBook/Language.md index 317f032..9b4363a 100644 --- a/FadeBasic/book/FadeBook/Language.md +++ b/FadeBasic/book/FadeBook/Language.md @@ -1,3 +1,54 @@ + +### Contents + +- [Inspiration](#language-guide) +- Language features + - [Comments](#comments) + - [Variables](#variables) + - [Single Line](#single-line-assignment) + - [Sigils](#sigils) + - [Casting](#casting) + - [Primitives](#primitive-types) + - [Implicit Casts](#implicit-casts) + - [Strings](#strings) + - [Functions](#functions) + - [Scopes](#function-scopes) + - [Returns](#return-values) + - [Parameters](#parameters) + - [Nested Functions](#no-nested-functions) + - [Clojures](#no-lambdas-or-clojures) + - [Scopes](#scopes) + - [Types](#user-defined-types) + - [Default](#udt-default-value) + - [Initializers](#udt-initializer) + - [Assignment](#udt-assignment) + - [Methods](#no-methods) + - [Arrays](#arrays) + - [Multidimensions](#multidimensional-arrays) + - [Arrays of Types](#arrays-of-udt) + - [Out of Bounds](#array-out-of-bounds) + - [Return Values](#cannot-return-arrays-from-functions) + - [Assignment](#cannot-assign-an-array) + - [Literals](#literals) + - [Operations](#operations) + - [Numerics](#numeric-operations) + - [Commands](#commands) + - [Short Circuits](#short-circuiting) + - [Control Statements](#control-statements) + - [Conditionals](#conditionals) + - [Single Line](#single-line-statements) + - [For Loops](#for-loops) + - [While Loops](#while-loops) + - [Repeat Loops](#repeat-loops) + - [Do Loops](#do-loops) + - [End](#end) + - [Goto](#goto) + - [Gosub](#gosub) + - [Select](#select) + - [Constants](#compile-time-constants) + - [Memory](#memory) + + # Language Guide _Fade Basic_ is a variant of BASIC. The language is fairly limited in its scope and it is intended to capture the essence of what _Dark Basic Pro_ was able to do in 2003. If you are familiar with _Dark Basic_, then read about the [Differences between _Fade Basic_ and _Dark Basic_](https://github.com/cdhanna/fadebasic/blob/main/FadeBasic/book/FadeBook/Dark%20Basic%20Pro%20Changes.md). It is worth glancing over this document with an open mind, as some of the language decisions may raise an eyebrow in 2025. @@ -114,7 +165,7 @@ x = val("42") `x is 42 ## Primitive Types -_Fade Basic_ supports the following primitives. The _classic name_ is inspired from the BASIC-era, and the _C# equivalent_ is the mapped type. At the moment, not all valid C# primitive types are available. However, it is valid to use either type name in your declarations. +_Fade Basic_ supports the following primitives. The _classic name_ is inspired from the BASIC-era, and the _C# equivalent_ is the mapped type. At the moment, not all valid C# primitive types are available. However, it is valid to use either type name in your declarations. The default value of all primtives is zero, except for `STRING`, which is an empty string. | classic name | C# equivalent | byte-size | description | range | | --------------| ------------- | --------- | ----------- | ----- | @@ -235,7 +286,7 @@ ENDFUNCTION ``` ---- -#### No Lambdas or Closures +#### No Lambdas or Clojures _Fade_ does not support function pointers or the ability to create a closure. @@ -287,6 +338,103 @@ ENDTYPE Be careful! It is not valid to have a recursive type dependency. In the example above, it would be incorrect to add an `EGG` field to the `CHICKEN` type. +---- +#### UDT Default Value + +An instance of a UDT can be reset back to an empty object using the `default` keyword. +```basic +TYPE VECTOR + x, + y +ENDTYPE + +v AS VECTOR +v.x = 4 +v.y = 2 + +` this line resets the object and clears all field values +v = default + +PRINT v.x + v.y `prints 0 +``` + +The `default` keyword can only be used in simple assignments and declarations. + +---- +#### UDT Initializer + +It is possible to set many fields at once by using object initializers. +```basic +TYPE VECTOR + x, + y +ENDTYPE + +v AS VECTOR = { + x = 1, + y = 2 +} +``` + +This syntax is equivelent to writing the assignments out one by one. The declaration above is equivelent to the following code snippet, +```basic +v AS VECTOR +v.x = 1 +v.y = 2 +``` + +Object initializers can be used in a nested construction as well. +```basic +TYPE CHICKEN + name$ +ENDTYPE +TYPE EGG + size + chicken AS CHICKEN +ENDTYPE +e AS EGG = { + size = 1, + chicken = { + name$ = "Albert" + } +} +``` + +It is also valid to use field accessors within the assignments of an object initializer. +```basic +TYPE CHICKEN + name$ +ENDTYPE +TYPE EGG + size + chicken AS CHICKEN +ENDTYPE +e AS EGG = { + size = 1, + chicken.name$ = "Albert" +} +``` + +When object initializers are used _after_ the initial declaration of an instance, they also reset the instance data to `default` before applying the initializer assignments. For example, the following example only prints "1". +```basic +TYPE VECTOR + x, + y +ENDTYPE + +v AS VECTOR = { + x = 4, + y = 7 +} + +v = { + y = 1 +} + +PRINT v.y + v.x `prints 1 +``` + + ---- #### UDT Assignment