diff --git a/README.md b/README.md index 366e39d..e73e5f6 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,22 @@ package main test::publicFn(); ``` +### Structs +```js +package main + +pub struct Point { + var x i32; + var y i32; +} + +var p = new Point(); +p.x = 2; +p.y = 2; + +print(p.x); // 2 +``` + ### Supports - [x] Int, bool, char, string, double, null diff --git a/Yail.Shared/Abstract/IInstantiable.cs b/Yail.Shared/Abstract/IInstantiable.cs new file mode 100644 index 0000000..2dafa75 --- /dev/null +++ b/Yail.Shared/Abstract/IInstantiable.cs @@ -0,0 +1,8 @@ +namespace Yail.Shared.Abstract; + +public interface IInstantiable +{ + void Set(string variableName, ValueObj value); + Dictionary Get(); + ValueObj Get(string propName); +} \ No newline at end of file diff --git a/Yail.Shared/Objects/StructObj.cs b/Yail.Shared/Objects/StructObj.cs new file mode 100644 index 0000000..b3caf32 --- /dev/null +++ b/Yail.Shared/Objects/StructObj.cs @@ -0,0 +1,33 @@ +using Yail.Shared.Abstract; + +namespace Yail.Shared.Objects; + +public class StructObj : ValueObj, IInstantiable +{ + public StructObj() + { + Value = new Dictionary(); + } + + public required string Name { get; set; } + public required bool IsPublic { get; set; } + + public void Set(string variableName, ValueObj value) + { + var list = Get(); + + list.TryAdd(variableName, value); + + Value = list; + } + + public Dictionary Get() + { + return Value as Dictionary; + } + + public ValueObj Get(string propName) + { + return (Value as Dictionary)[propName]; + } +} \ No newline at end of file diff --git a/Yail.Shared/ValueObj.cs b/Yail.Shared/ValueObj.cs index f0c8146..8c5a1bf 100644 --- a/Yail.Shared/ValueObj.cs +++ b/Yail.Shared/ValueObj.cs @@ -12,6 +12,31 @@ public ValueObj(object? value, EDataType dataType, bool isConst = false) DataType = dataType; IsConst = isConst; } + + public ValueObj(EDataType dataType, bool isConst = false) + { + DataType = dataType; + IsConst = isConst; + + switch (dataType) + { + case EDataType.Int32: + Value = 0; + break; + case EDataType.Boolean: + Value = false; + break; + case EDataType.String: + Value = string.Empty; + break; + case EDataType.Double: + Value = 0.0D; + break; + default: + Value = null; + break; + } + } public bool IsConst { get; set; } public object? Value { get; set; } diff --git a/Yail.Tests/IOTests.cs b/Yail.Tests/IOTests.cs new file mode 100644 index 0000000..78bf81c --- /dev/null +++ b/Yail.Tests/IOTests.cs @@ -0,0 +1,43 @@ +using Antlr4.Runtime; +using Yail.Grammar; + +namespace Yail.Tests; + +public class IOTests +{ + private string RunCode(string code, string input = "") + { + using var output = new StringWriter(); + Console.SetOut(output); + + using var inputReader = new StringReader(input); + Console.SetIn(inputReader); + + var inputStream = new AntlrInputStream(code); + var lexer = new ExpressionsLexer(inputStream); + var tokenStream = new CommonTokenStream(lexer); + var parser = new ExpressionsParser(tokenStream); + + var tree = parser.program(); + + var visitor = new ExpressionsVisitor(); + visitor.Visit(tree); + + return output.ToString(); + } + + [Test] + public void TestInputOutput() + { + var code = @" + println(""Enter your name:""); + var name = input(); + println(""Hello, "" + name + ""!""); + "; + + var simulatedInput = "Bob"; + var output = RunCode(code, simulatedInput); + + Assert.That(output, Is.EqualTo("Enter your name:\r\nHello, Bob!\r\n")); + } +} \ No newline at end of file diff --git a/Yail.Tests/OperationsTest.cs b/Yail.Tests/OperationsTest.cs new file mode 100644 index 0000000..1c632f6 --- /dev/null +++ b/Yail.Tests/OperationsTest.cs @@ -0,0 +1,98 @@ +using Antlr4.Runtime; +using Yail.Grammar; + +namespace Yail.Tests; + +[TestFixture] +public class OperationsTest +{ + private string RunCode(string code) + { + using var output = new StringWriter(); + Console.SetOut(output); + + var inputStream = new AntlrInputStream(code); + var lexer = new ExpressionsLexer(inputStream); + var tokenStream = new CommonTokenStream(lexer); + var parser = new ExpressionsParser(tokenStream); + + var tree = parser.program(); + + var visitor = new ExpressionsVisitor(); + visitor.Visit(tree); + + return output.ToString(); + } + + [Test] + public void TestAddition() + { + var code = @" + var x = 3; + var y = x + 3; + println(y); + "; + + var actual = RunCode(code); + + Assert.That(actual, Is.EqualTo("6\r\n")); + } + + [Test] + public void TestSubtraction() + { + var code = @" + var x = 5; + var y = x - 3; + println(y); + "; + + var actual = RunCode(code); + + Assert.That(actual, Is.EqualTo("2\r\n")); + } + + [Test] + public void TestMultiplication() + { + var code = @" + var x = 5; + var y = x * 3; + println(y); + "; + + var actual = RunCode(code); + + Assert.That(actual, Is.EqualTo("15\r\n")); + } + + [Test] + public void TestDivision() + { + var code = @" + var x = 20; + var y = x / 4; + println(y); + "; + + var actual = RunCode(code); + + Assert.That(actual, Is.EqualTo("5\r\n")); + } + + [Test] + public void TestModulo() + { + var code = @" + var x = 20; + var y = x % 2; + var z = 15 % 2; + println(y); + println(z); + "; + + var actual = RunCode(code); + + Assert.That(actual, Is.EqualTo("0\r\n1\r\n")); + } +} \ No newline at end of file diff --git a/Yail.Tests/UnitTest1.cs b/Yail.Tests/UnitTest1.cs deleted file mode 100644 index 5807b25..0000000 --- a/Yail.Tests/UnitTest1.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Yail.Tests; - -public class Tests -{ - private ExpressionsVisitor Sut { get; set; } - - [SetUp] - public void Setup() - { - Sut = new ExpressionsVisitor(); - } - - [Test] - public void Test1() - { - Assert.Pass(); - } -} \ No newline at end of file diff --git a/Yail/ExpressionsVisitor.cs b/Yail/ExpressionsVisitor.cs index ceb1bf6..ce1d3ac 100644 --- a/Yail/ExpressionsVisitor.cs +++ b/Yail/ExpressionsVisitor.cs @@ -1,5 +1,4 @@ -using System.Net.Http.Headers; -using Yail.Common; +using Yail.Common; using Yail.Common.Extentions; using Yail.Grammar; using Yail.Shared; @@ -13,6 +12,7 @@ namespace Yail; public sealed class ExpressionsVisitor : ExpressionsBaseVisitor { private Dictionary _variables = new(); + private Dictionary _instances = new(); private Dictionary _functions = new(); private ValueObj? returnValueFromFunction; private readonly HashSet _activeDirectives = new(); @@ -168,23 +168,6 @@ public sealed class ExpressionsVisitor : ExpressionsBaseVisitor return newValue; } - - private ValueObj PerformOperation(string operation, ValueObj valueObj) - { - if (valueObj.DataType != EDataType.Int32 && valueObj.DataType != EDataType.Double) - { - throw new InvalidOperationException($"Operation '{operation}' is not supported for type '{valueObj.DataType}'."); - } - - return operation switch - { - "++" => OperationsHelper.Add(valueObj, new ValueObj { DataType = valueObj.DataType, Value = 1 }), - "--" => OperationsHelper.Subtract(valueObj, new ValueObj { DataType = valueObj.DataType, Value = 1 }), - "**" => OperationsHelper.Power(valueObj, new ValueObj { DataType = valueObj.DataType, Value = 2 }), - "//" => OperationsHelper.FloorDivide(valueObj, new ValueObj { DataType = valueObj.DataType, Value = 2 }), - _ => throw new InvalidOperationException($"Unknown operation '{operation}'.") - }; - } public override ValueObj? VisitIdentifierExpr(ExpressionsParser.IdentifierExprContext context) { @@ -1023,8 +1006,113 @@ public override ValueObj VisitDictionaryLiteral(ExpressionsParser.DictionaryLite #endregion + #region Structs + + public override ValueObj VisitStructBlock(ExpressionsParser.StructBlockContext context) + { + var structName = context.IDENTIFIER().GetText(); + var isPublic = context.accessLevels()?.GetText()?.Equals("pub") ?? false; + + var structObj = new StructObj + { + Name = structName, + IsPublic = isPublic + }; + + foreach (var structLine in context.structBody().structLine()) + { + var varName = structLine.variableDefine().IDENTIFIER().GetText(); + var dataType = structLine.variableDefine().DATA_TYPES().GetText().ToDataType(); + + structObj.Set(varName, new ValueObj(dataType)); + } + + if (!_instances.TryAdd(structObj.Name, structObj)) + { + throw new Exception("Instance with this identifier already exists."); + } + + return structObj; + } + + public override ValueObj? VisitInstanceCreateExpr(ExpressionsParser.InstanceCreateExprContext context) + { + var instanceName = context.instanceCreate().IDENTIFIER().GetText(); + var valueObj = _instances[instanceName]; + + valueObj.ThrowIfNull(); + + if (!_variables.TryAdd(instanceName, valueObj)) + { + ExceptionHelper.PrintError($"Variable with name '{instanceName}' is already declared"); + } + + return valueObj; + } + + public override ValueObj? VisitInstancePropAssign(ExpressionsParser.InstancePropAssignContext context) + { + var instanceName = context.IDENTIFIER(0).GetText(); + + var valueObj = _variables[instanceName]; + + valueObj.ThrowIfNull(); + + if (valueObj is not IInstantiable instance) + { + throw new Exception("This expression is only valid on instances."); + } + + var propName = context.IDENTIFIER(1).GetText(); + + var prop = instance.Get(propName); + + var value = Visit(context.expression()); + value.ThrowIfNull(); + + prop.Value = value!.Value; + + return null; + } + + public override ValueObj VisitInstancePropCallExpr(ExpressionsParser.InstancePropCallExprContext context) + { + var instanceName = context.instancePropCall().IDENTIFIER(0).GetText(); + var propName = context.instancePropCall().IDENTIFIER(1).GetText(); + + var valueObj = _variables[instanceName]; + + valueObj.ThrowIfNull(); + + if (valueObj is not IInstantiable instance) + { + throw new Exception("This expression is only valid on instances."); + } + + return instance.Get(propName); + } + + #endregion + #region Helpers + private ValueObj PerformOperation(string operation, ValueObj valueObj) + { + if (valueObj.DataType != EDataType.Int32 && valueObj.DataType != EDataType.Double) + { + throw new InvalidOperationException($"Operation '{operation}' is not supported for type '{valueObj.DataType}'."); + } + + return operation switch + { + "++" => OperationsHelper.Add(valueObj, new ValueObj { DataType = valueObj.DataType, Value = 1 }), + "--" => OperationsHelper.Subtract(valueObj, new ValueObj { DataType = valueObj.DataType, Value = 1 }), + "**" => OperationsHelper.Power(valueObj, new ValueObj { DataType = valueObj.DataType, Value = 2 }), + "//" => OperationsHelper.FloorDivide(valueObj, new ValueObj { DataType = valueObj.DataType, Value = 2 }), + _ => throw new InvalidOperationException($"Unknown operation '{operation}'.") + }; + } + private bool EvaluateCondition(ExpressionsParser.ExpressionContext conditionContext) { var conditionResult = Visit(conditionContext); diff --git a/Yail/Grammar/Expressions.g4 b/Yail/Grammar/Expressions.g4 index a6f2192..05319c6 100644 --- a/Yail/Grammar/Expressions.g4 +++ b/Yail/Grammar/Expressions.g4 @@ -18,8 +18,12 @@ USE_IDENTIFIERS: 'disable-type-checking'; // grammar program: line* EOF; -line: packageDeclaration | usingDirective | classBlock | directive | statement | ifBlock | whileBlock | forBlock | foreachBlock | functionDeclaration | return; -block: '{' line* '}'; +line: packageDeclaration | usingDirective | structBlock | directive | statement | ifBlock | whileBlock | forBlock | foreachBlock | functionDeclaration | return; +block: '{' line* '}'; + +structBody: '{' structLine* '}'; +structLine: variableDefine ';'; + directive: '#' 'use' USE_IDENTIFIERS; multiplyOp: '*' | '/' | '%'; @@ -55,12 +59,15 @@ expression | '(' DATA_TYPES ')' expression #castExpr | arrayLength #arrayLengthExpr | dictionaryLiteral #dictionaryLiteralExpr // x = {} + | instanceCreate #instanceCreateExpr + | instancePropCall #instancePropCallExpr ; packageDeclaration: 'package' IDENTIFIER; usingDirective: 'using' IDENTIFIER; variableDeclaration: 'var' IDENTIFIER '=' REFERENCE? expression; +variableDefine: 'var' IDENTIFIER DATA_TYPES; functionDeclaration: (accessLevels)? 'funky' IDENTIFIER '(' (parameterList)? ')' DATA_TYPES block; parameterList: parameter (',' parameter)*; @@ -78,7 +85,7 @@ functionCall | IDENTIFIER '.' IDENTIFIER '(' (expression (',' expression)*)? ')' # methodCall ; -statement: (variableDeclaration | assignment | operationAssignment | selfOperation | functionCall | break | continue | return) ';'; +statement: (variableDeclaration | assignment | operationAssignment | selfOperation | functionCall | instancePropAssign | break | continue | return) ';'; // Blocks ifBlock: 'if' '('? expression ')'? block ('else' elseIfBlock)?; @@ -88,4 +95,8 @@ whileBlock: WHILE '('? expression ')'? block; forBlock: FOR '('? (variableDeclaration | assignment)? ';' expression? ';' (selfOperation)? ')'? block; foreachBlock: FOREACH '(' 'var' IDENTIFIER 'in' expression ')' block; -classBlock: accessLevels? 'class' IDENTIFIER block; // TODO: future \ No newline at end of file +structBlock: accessLevels? 'struct' IDENTIFIER structBody; + +instanceCreate: 'new' IDENTIFIER '(' (expression (',' expression)*)? ')'; +instancePropAssign: IDENTIFIER '.' IDENTIFIER '=' expression; +instancePropCall: IDENTIFIER '.' IDENTIFIER; \ No newline at end of file diff --git a/Yail/Samples/main.yail b/Yail/Samples/main.yail index 8cc0778..1685512 100644 --- a/Yail/Samples/main.yail +++ b/Yail/Samples/main.yail @@ -1,13 +1,12 @@ package main -var dict = {"key1": 1, "key2": 2}; +pub struct Point { + var x i32; + var y i32; +} -var x = &dict["key1"]; +var p = new Point(); +p.x = 2; +p.y = 2; -println(x); // 1 - -dict["key1"] = 2; - -println(x); // 1 - -print(dict); \ No newline at end of file +print(p.x); \ No newline at end of file