Implement subtraction
This commit is contained in:
		| @@ -37,12 +37,13 @@ int generate(string inFile, ref String outputFilename) @nogc | |||||||
|         return 3; |         return 3; | ||||||
|     } |     } | ||||||
|     auto tokens = lex(sourceText.get.get); |     auto tokens = lex(sourceText.get.get); | ||||||
|     if (tokens.length == 0) |     if (!tokens.valid) | ||||||
|     { |     { | ||||||
|         printf("Lexical analysis failed.\n"); |         auto compileError = tokens.error.get; | ||||||
|  |         printf("%lu:%lu: %s\n", compileError.line, compileError.column, compileError.message.ptr); | ||||||
|         return 1; |         return 1; | ||||||
|     } |     } | ||||||
|     auto ast = parse(tokens); |     auto ast = parse(tokens.result); | ||||||
|     if (!ast.valid) |     if (!ast.valid) | ||||||
|     { |     { | ||||||
|         auto compileError = ast.error.get; |         auto compileError = ast.error.get; | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import tanya.container.array; | |||||||
| import tanya.container.hashtable; | import tanya.container.hashtable; | ||||||
| import tanya.container.string; | import tanya.container.string; | ||||||
| import tanya.memory.allocator; | import tanya.memory.allocator; | ||||||
| import tanya.memory.mmappool; | public import elna.parser : BinaryOperator; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * IR visitor. |  * IR visitor. | ||||||
| @@ -19,7 +19,7 @@ abstract class IRVisitor | |||||||
|     abstract void visit(Variable) @nogc; |     abstract void visit(Variable) @nogc; | ||||||
|     abstract void visit(VariableDeclaration) @nogc; |     abstract void visit(VariableDeclaration) @nogc; | ||||||
|     abstract void visit(Number) @nogc; |     abstract void visit(Number) @nogc; | ||||||
|     abstract void visit(Subroutine) @nogc; |     abstract void visit(BinaryExpression) @nogc; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -47,7 +47,7 @@ class Definition : Node | |||||||
|  |  | ||||||
| class Statement : Node | class Statement : Node | ||||||
| { | { | ||||||
|     Subroutine subroutine; |     BinaryExpression expression; | ||||||
|  |  | ||||||
|     override void accept(IRVisitor visitor) @nogc |     override void accept(IRVisitor visitor) @nogc | ||||||
|     { |     { | ||||||
| @@ -93,9 +93,18 @@ class VariableDeclaration : Node | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| class Subroutine : Node | class BinaryExpression : Node | ||||||
| { | { | ||||||
|     Expression lhs, rhs; |     Expression lhs, rhs; | ||||||
|  |     BinaryOperator operator; | ||||||
|  |  | ||||||
|  |     this(Expression lhs, Expression rhs, BinaryOperator operator) | ||||||
|  |     @nogc | ||||||
|  |     { | ||||||
|  |         this.lhs = lhs; | ||||||
|  |         this.rhs = rhs; | ||||||
|  |         this.operator = operator; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     override void accept(IRVisitor visitor) @nogc |     override void accept(IRVisitor visitor) @nogc | ||||||
|     { |     { | ||||||
| @@ -105,48 +114,50 @@ class Subroutine : Node | |||||||
|  |  | ||||||
| private Number transformNumber(parser.Number number) @nogc | private Number transformNumber(parser.Number number) @nogc | ||||||
| { | { | ||||||
|     return MmapPool.instance.make!Number(number.value); |     return defaultAllocator.make!Number(number.value); | ||||||
| } | } | ||||||
|  |  | ||||||
| private Variable transformSubroutine(parser.Subroutine subroutine, | private Variable binaryExpression(parser.BinaryExpression binaryExpression, | ||||||
|         ref Array!Statement statements, |         ref Array!Statement statements, | ||||||
|         ref HashTable!(String, int) constants) @nogc |         ref HashTable!(String, int) constants) @nogc | ||||||
| { | { | ||||||
|     auto target = MmapPool.instance.make!Subroutine; |     auto target = defaultAllocator.make!BinaryExpression( | ||||||
|     target.lhs = transformExpression(subroutine.lhs, statements, constants); |         expression(binaryExpression.lhs, statements, constants), | ||||||
|     target.rhs = transformExpression(subroutine.rhs, statements, constants); |         expression(binaryExpression.rhs, statements, constants), | ||||||
|  |         binaryExpression.operator | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     auto newStatement = MmapPool.instance.make!Statement; |     auto newStatement = defaultAllocator.make!Statement; | ||||||
|     newStatement.subroutine = target; |     newStatement.expression = target; | ||||||
|     statements.insertBack(newStatement); |     statements.insertBack(newStatement); | ||||||
|  |  | ||||||
|     auto newVariable = MmapPool.instance.make!Variable; |     auto newVariable = defaultAllocator.make!Variable; | ||||||
|     newVariable.counter = statements.length; |     newVariable.counter = statements.length; | ||||||
|  |  | ||||||
|     return newVariable; |     return newVariable; | ||||||
| } | } | ||||||
|  |  | ||||||
| private Expression transformExpression(parser.Expression expression, | private Expression expression(parser.Expression expression, | ||||||
|         ref Array!Statement statements, |         ref Array!Statement statements, | ||||||
|         ref HashTable!(String, int) constants) @nogc |         ref HashTable!(String, int) constants) @nogc | ||||||
| { | { | ||||||
|     if ((cast(parser.Number) expression) !is null) |     if ((cast(parser.Number) expression) !is null) | ||||||
|     { |     { | ||||||
|         auto numberExpression = MmapPool.instance.make!Number; |         auto numberExpression = defaultAllocator.make!Number; | ||||||
|         numberExpression.value = (cast(parser.Number) expression).value; |         numberExpression.value = (cast(parser.Number) expression).value; | ||||||
|  |  | ||||||
|         return numberExpression; |         return numberExpression; | ||||||
|     } |     } | ||||||
|     if ((cast(parser.Variable) expression) !is null) |     if ((cast(parser.Variable) expression) !is null) | ||||||
|     { |     { | ||||||
|         auto numberExpression = MmapPool.instance.make!Number; |         auto numberExpression = defaultAllocator.make!Number; | ||||||
|         numberExpression.value = constants[(cast(parser.Variable) expression).identifier]; |         numberExpression.value = constants[(cast(parser.Variable) expression).identifier]; | ||||||
|  |  | ||||||
|         return numberExpression; |         return numberExpression; | ||||||
|     } |     } | ||||||
|     else if ((cast(parser.Subroutine) expression) !is null) |     else if ((cast(parser.BinaryExpression) expression) !is null) | ||||||
|     { |     { | ||||||
|         return transformSubroutine(cast(parser.Subroutine) expression, statements, constants); |         return binaryExpression(cast(parser.BinaryExpression) expression, statements, constants); | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
| } | } | ||||||
| @@ -157,7 +168,7 @@ Expression transformStatement(parser.Statement statement, | |||||||
| { | { | ||||||
|     if ((cast(parser.BangStatement) statement) !is null) |     if ((cast(parser.BangStatement) statement) !is null) | ||||||
|     { |     { | ||||||
|         return transformExpression((cast(parser.BangStatement) statement).expression, statements, constants); |         return expression((cast(parser.BangStatement) statement).expression, statements, constants); | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
| } | } | ||||||
| @@ -181,7 +192,7 @@ Array!VariableDeclaration transformVariableDeclarations(ref Array!(parser.Variab | |||||||
|  |  | ||||||
|     foreach (ref variableDeclaration; variableDeclarations) |     foreach (ref variableDeclaration; variableDeclarations) | ||||||
|     { |     { | ||||||
|         auto newDeclaration = MmapPool.instance.make!VariableDeclaration; |         auto newDeclaration = defaultAllocator.make!VariableDeclaration; | ||||||
|         newDeclaration.identifier = variableDeclaration.identifier; |         newDeclaration.identifier = variableDeclaration.identifier; | ||||||
|         variables.insertBack(newDeclaration); |         variables.insertBack(newDeclaration); | ||||||
|     } |     } | ||||||
| @@ -191,7 +202,7 @@ Array!VariableDeclaration transformVariableDeclarations(ref Array!(parser.Variab | |||||||
|  |  | ||||||
| Definition transform(parser.Block block) @nogc | Definition transform(parser.Block block) @nogc | ||||||
| { | { | ||||||
|     auto target = MmapPool.instance.make!Definition; |     auto target = defaultAllocator.make!Definition; | ||||||
|     auto constants = transformConstants(block.definitions); |     auto constants = transformConstants(block.definitions); | ||||||
|  |  | ||||||
|     transformStatement(block.statement, target.statements, constants); |     transformStatement(block.statement, target.statements, constants); | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import elna.result; | |||||||
| import std.range; | import std.range; | ||||||
| import tanya.container.array; | import tanya.container.array; | ||||||
| import tanya.container.string; | import tanya.container.string; | ||||||
| import tanya.memory.mmappool; |  | ||||||
|  |  | ||||||
| struct Token | struct Token | ||||||
| { | { | ||||||
| @@ -54,7 +53,7 @@ struct Token | |||||||
|  |  | ||||||
|     this()(Type type, auto ref String value, Position position) |     this()(Type type, auto ref String value, Position position) | ||||||
|     @nogc nothrow pure @trusted |     @nogc nothrow pure @trusted | ||||||
|     in (type == Type.identifier) |     in (type == Type.identifier || type == Type.operator) | ||||||
|     { |     { | ||||||
|         this(type, position); |         this(type, position); | ||||||
|         this.value_.identifier = value; |         this.value_.identifier = value; | ||||||
| @@ -78,7 +77,7 @@ struct Token | |||||||
|         { |         { | ||||||
|             return this.value_.number; |             return this.value_.number; | ||||||
|         } |         } | ||||||
|         else static if (type == Type.identifier) |         else static if (type == Type.identifier || type == Type.operator) | ||||||
|         { |         { | ||||||
|             return this.value_.identifier; |             return this.value_.identifier; | ||||||
|         } |         } | ||||||
| @@ -161,7 +160,7 @@ struct Source | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| Array!Token lex(char[] buffer) @nogc | Result!(Array!Token) lex(char[] buffer) @nogc | ||||||
| { | { | ||||||
|     Array!Token tokens; |     Array!Token tokens; | ||||||
|     auto source = Source(buffer); |     auto source = Source(buffer); | ||||||
| @@ -234,9 +233,12 @@ Array!Token lex(char[] buffer) @nogc | |||||||
|             } |             } | ||||||
|             source.popFrontN(i); |             source.popFrontN(i); | ||||||
|         } |         } | ||||||
|         else if (source.front == '+') // Multi-character, random special characters. |         else if (source.front == '+' || source.front == '-') | ||||||
|         { |         { | ||||||
|             tokens.insertBack(Token(Token.Type.operator, source.position)); |             String operator; | ||||||
|  |  | ||||||
|  |             operator.insertBack(source.front); | ||||||
|  |             tokens.insertBack(Token(Token.Type.operator, operator, source.position)); | ||||||
|             source.popFront; |             source.popFront; | ||||||
|         } |         } | ||||||
|         else if (source.front == '\n') |         else if (source.front == '\n') | ||||||
| @@ -245,8 +247,8 @@ Array!Token lex(char[] buffer) @nogc | |||||||
|         } |         } | ||||||
|         else |         else | ||||||
|         { |         { | ||||||
|             return typeof(tokens)(); // Error. |             return typeof(return)("Unexptected next character", source.position); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     return tokens; |     return typeof(return)(tokens); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import elna.result; | |||||||
| import tanya.container.array; | import tanya.container.array; | ||||||
| import tanya.container.string; | import tanya.container.string; | ||||||
| import tanya.memory.allocator; | import tanya.memory.allocator; | ||||||
| import tanya.memory.mmappool; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Constant definition. |  * Constant definition. | ||||||
| @@ -54,9 +53,34 @@ class Variable : Expression | |||||||
|     String identifier; |     String identifier; | ||||||
| } | } | ||||||
|  |  | ||||||
| class Subroutine : Expression | enum BinaryOperator | ||||||
|  | { | ||||||
|  |     sum, | ||||||
|  |     subtraction | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class BinaryExpression : Expression | ||||||
| { | { | ||||||
|     Expression lhs, rhs; |     Expression lhs, rhs; | ||||||
|  |     BinaryOperator operator; | ||||||
|  |  | ||||||
|  |     this(Expression lhs, Expression rhs, String operator) @nogc | ||||||
|  |     { | ||||||
|  |         this.lhs = lhs; | ||||||
|  |         this.rhs = rhs; | ||||||
|  |         if (operator == "+") | ||||||
|  |         { | ||||||
|  |             this.operator = BinaryOperator.sum; | ||||||
|  |         } | ||||||
|  |         else if (operator == "-") | ||||||
|  |         { | ||||||
|  |             this.operator = BinaryOperator.subtraction; | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             assert(false, "Invalid binary operator"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| private Result!Expression parseFactor(ref Array!Token.Range tokens) @nogc | private Result!Expression parseFactor(ref Array!Token.Range tokens) @nogc | ||||||
| @@ -64,14 +88,14 @@ in (!tokens.empty, "Expected factor, got end of stream") | |||||||
| { | { | ||||||
|     if (tokens.front.ofType(Token.Type.identifier)) |     if (tokens.front.ofType(Token.Type.identifier)) | ||||||
|     { |     { | ||||||
|         auto variable = MmapPool.instance.make!Variable; |         auto variable = defaultAllocator.make!Variable; | ||||||
|         variable.identifier = tokens.front.value!(Token.Type.identifier); |         variable.identifier = tokens.front.value!(Token.Type.identifier); | ||||||
|         tokens.popFront; |         tokens.popFront; | ||||||
|         return Result!Expression(variable); |         return Result!Expression(variable); | ||||||
|     } |     } | ||||||
|     else if (tokens.front.ofType(Token.Type.number)) |     else if (tokens.front.ofType(Token.Type.number)) | ||||||
|     { |     { | ||||||
|         auto number = MmapPool.instance.make!Number; |         auto number = defaultAllocator.make!Number; | ||||||
|         number.value = tokens.front.value!(Token.Type.number); |         number.value = tokens.front.value!(Token.Type.number); | ||||||
|         tokens.popFront; |         tokens.popFront; | ||||||
|         return Result!Expression(number); |         return Result!Expression(number); | ||||||
| @@ -101,17 +125,17 @@ in (!tokens.empty, "Expected expression, got end of stream") | |||||||
|     { |     { | ||||||
|         return term; |         return term; | ||||||
|     } |     } | ||||||
|  |     auto operator = tokens.front.value!(Token.Type.operator); | ||||||
|     tokens.popFront; |     tokens.popFront; | ||||||
|  |  | ||||||
|     auto operator = MmapPool.instance.make!Subroutine; |  | ||||||
|     auto expression = parseExpression(tokens); |     auto expression = parseExpression(tokens); | ||||||
|  |  | ||||||
|     if (expression.valid) |     if (expression.valid) | ||||||
|     { |     { | ||||||
|         operator.lhs = term.result; |         auto binaryExpression = defaultAllocator | ||||||
|         operator.rhs = expression.result; |             .make!BinaryExpression(term.result, expression.result, operator); | ||||||
|  |  | ||||||
|         return Result!Expression(operator); |         return Result!Expression(binaryExpression); | ||||||
|     } |     } | ||||||
|     else |     else | ||||||
|     { |     { | ||||||
| @@ -122,7 +146,7 @@ in (!tokens.empty, "Expected expression, got end of stream") | |||||||
| private Result!Definition parseDefinition(ref Array!Token.Range tokens) @nogc | private Result!Definition parseDefinition(ref Array!Token.Range tokens) @nogc | ||||||
| in (!tokens.empty, "Expected definition, got end of stream") | in (!tokens.empty, "Expected definition, got end of stream") | ||||||
| { | { | ||||||
|     auto definition = MmapPool.instance.make!Definition; |     auto definition = defaultAllocator.make!Definition; | ||||||
|     definition.identifier = tokens.front.value!(Token.Type.identifier); // Copy. |     definition.identifier = tokens.front.value!(Token.Type.identifier); // Copy. | ||||||
|  |  | ||||||
|     tokens.popFront(); |     tokens.popFront(); | ||||||
| @@ -130,7 +154,7 @@ in (!tokens.empty, "Expected definition, got end of stream") | |||||||
|  |  | ||||||
|     if (tokens.front.ofType(Token.Type.number)) |     if (tokens.front.ofType(Token.Type.number)) | ||||||
|     { |     { | ||||||
|         auto number = MmapPool.instance.make!Number; |         auto number = defaultAllocator.make!Number; | ||||||
|         number.value = tokens.front.value!(Token.Type.number); |         number.value = tokens.front.value!(Token.Type.number); | ||||||
|         definition.number = number; |         definition.number = number; | ||||||
|         tokens.popFront; |         tokens.popFront; | ||||||
| @@ -145,7 +169,7 @@ in (!tokens.empty, "Expected block, got end of stream") | |||||||
|     if (tokens.front.ofType(Token.Type.bang)) |     if (tokens.front.ofType(Token.Type.bang)) | ||||||
|     { |     { | ||||||
|         tokens.popFront; |         tokens.popFront; | ||||||
|         auto statement = MmapPool.instance.make!BangStatement; |         auto statement = defaultAllocator.make!BangStatement; | ||||||
|         auto expression = parseExpression(tokens); |         auto expression = parseExpression(tokens); | ||||||
|         if (expression.valid) |         if (expression.valid) | ||||||
|         { |         { | ||||||
| @@ -200,7 +224,7 @@ in (!tokens.empty, "Expected variable declarations, got end of stream") | |||||||
|         auto currentToken = tokens.front; |         auto currentToken = tokens.front; | ||||||
|         if (currentToken.ofType(Token.Type.identifier)) |         if (currentToken.ofType(Token.Type.identifier)) | ||||||
|         { |         { | ||||||
|             auto variableDeclaration = MmapPool.instance.make!VariableDeclaration; |             auto variableDeclaration = defaultAllocator.make!VariableDeclaration; | ||||||
|             variableDeclaration.identifier = currentToken.value!(Token.Type.identifier); |             variableDeclaration.identifier = currentToken.value!(Token.Type.identifier); | ||||||
|             variableDeclarations.insertBack(variableDeclaration); |             variableDeclarations.insertBack(variableDeclaration); | ||||||
|             tokens.popFront; |             tokens.popFront; | ||||||
| @@ -229,7 +253,7 @@ in (!tokens.empty, "Expected variable declarations, got end of stream") | |||||||
| private Result!Block parseBlock(ref Array!Token.Range tokens) @nogc | private Result!Block parseBlock(ref Array!Token.Range tokens) @nogc | ||||||
| in (!tokens.empty, "Expected block, got end of stream") | in (!tokens.empty, "Expected block, got end of stream") | ||||||
| { | { | ||||||
|     auto block = MmapPool.instance.make!Block; |     auto block = defaultAllocator.make!Block; | ||||||
|     if (tokens.front.ofType(Token.Type.let)) |     if (tokens.front.ofType(Token.Type.let)) | ||||||
|     { |     { | ||||||
|         auto constDefinitions = parseDefinitions(tokens); |         auto constDefinitions = parseDefinitions(tokens); | ||||||
|   | |||||||
| @@ -98,6 +98,12 @@ enum Funct12 : ubyte | |||||||
|     ebreak = 0b000000000001, |     ebreak = 0b000000000001, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | enum Funct7 : ubyte | ||||||
|  | { | ||||||
|  |     none = 0, | ||||||
|  |     sub = 0b0100000 | ||||||
|  | } | ||||||
|  |  | ||||||
| enum BaseOpcode : ubyte | enum BaseOpcode : ubyte | ||||||
| { | { | ||||||
|     opImm = 0b0010011, |     opImm = 0b0010011, | ||||||
| @@ -146,7 +152,7 @@ struct Instruction | |||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     ref Instruction r(XRegister rd, Funct3 funct3, XRegister rs1, XRegister rs2, ubyte funct7 = 0) |     ref Instruction r(XRegister rd, Funct3 funct3, XRegister rs1, XRegister rs2, Funct7 funct7 = Funct7.none) | ||||||
|     return scope @nogc |     return scope @nogc | ||||||
|     { |     { | ||||||
|         this.instruction |= (rd << 7) |         this.instruction |= (rd << 7) | ||||||
| @@ -263,7 +269,7 @@ class RiscVVisitor : IRVisitor | |||||||
|  |  | ||||||
|     override void visit(Statement statement) @nogc |     override void visit(Statement statement) @nogc | ||||||
|     { |     { | ||||||
|         statement.subroutine.accept(this); |         statement.expression.accept(this); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override void visit(Variable variable) @nogc |     override void visit(Variable variable) @nogc | ||||||
| @@ -292,18 +298,29 @@ class RiscVVisitor : IRVisitor | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override void visit(Subroutine subroutine) @nogc |     override void visit(BinaryExpression expression) @nogc | ||||||
|     { |     { | ||||||
|         this.registerInUse = true; |         this.registerInUse = true; | ||||||
|         subroutine.lhs.accept(this); |         expression.lhs.accept(this); | ||||||
|         this.registerInUse = false; |         this.registerInUse = false; | ||||||
|         subroutine.rhs.accept(this); |         expression.rhs.accept(this); | ||||||
|  |  | ||||||
|         // Calculate the result and assign it to a variable on the stack. |         // Calculate the result and assign it to a variable on the stack. | ||||||
|         this.instructions.insertBack( |         final switch (expression.operator) | ||||||
|             Instruction(BaseOpcode.op) |         { | ||||||
|                 .r(XRegister.a0, Funct3.add, XRegister.a0, XRegister.t0) |             case BinaryOperator.sum: | ||||||
|         ); |                 this.instructions.insertBack( | ||||||
|  |                     Instruction(BaseOpcode.op) | ||||||
|  |                         .r(XRegister.a0, Funct3.add, XRegister.a0, XRegister.t0) | ||||||
|  |                 ); | ||||||
|  |                 break; | ||||||
|  |             case BinaryOperator.subtraction: | ||||||
|  |                 this.instructions.insertBack( | ||||||
|  |                     Instruction(BaseOpcode.op) | ||||||
|  |                         .r(XRegister.a0, Funct3.sub, XRegister.a0, XRegister.t0, Funct7.sub) | ||||||
|  |                 ); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|         this.instructions.insertBack( // movl %eax, -x(%rbp); where x is a number. |         this.instructions.insertBack( // movl %eax, -x(%rbp); where x is a number. | ||||||
|             Instruction(BaseOpcode.store) |             Instruction(BaseOpcode.store) | ||||||
|                 .s(cast(uint) (this.variableCounter * 4), Funct3.sw, XRegister.sp, XRegister.a0) |                 .s(cast(uint) (this.variableCounter * 4), Funct3.sw, XRegister.sp, XRegister.a0) | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								tests/expectations/subtraction.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/expectations/subtraction.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | 1 | ||||||
							
								
								
									
										2
									
								
								tests/subtraction.eln
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tests/subtraction.eln
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | ! 5 - 4 | ||||||
|  | . | ||||||
		Reference in New Issue
	
	Block a user