From f3c9865da8678f80a14c8c37525a3ae0f3686e53 Mon Sep 17 00:00:00 2001 From: Jason Whittington Date: Sun, 15 Mar 2026 09:23:56 -0700 Subject: [PATCH] Add mirror | (staple) and invert - (worm) operators with stacking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce two new unary operators: - | (horizontal mirror / staple): bit-reversal within variable width - - (vertical mirror / worm): ones' complement (bit inversion) These are perpendicular mirrors — | reflects left-right, - reflects top-bottom. They commute, and |-|- is identity, which means # has always been |-|-. All six unary operators (& V v ? | -) can now be stacked in any order, evaluated right-to-left (e.g., '&-|?.1'). Parser refactored from single unary op to list-based stacking across all three expression contexts. Enables future syslib division (| for MSB-first iteration, - for complement in subtraction) — completing INTERCAL's four basic arithmetic ops. Co-Authored-By: Claude Opus 4.6 (1M context) --- cringe/Token.cs | 2 +- cringe/Tokenizer.cs | 2 +- cringe/perplex.cs | 288 ++++++++++++--------------------- intercal.runtime/utils.cs | 53 ++++++ intercal.tests/LibTests.cs | 132 +++++++++++++++ intercal.tests/ProgramTests.cs | 82 ++++++++++ intercal.tests/ScannerTests.cs | 32 ++++ 7 files changed, 400 insertions(+), 191 deletions(-) diff --git a/cringe/Token.cs b/cringe/Token.cs index 32cdd28..359157f 100644 --- a/cringe/Token.cs +++ b/cringe/Token.cs @@ -9,7 +9,7 @@ public enum TokenType Statement, // READ OUT, WRITE IN, COME FROM, ABSTAIN FROM, GIVE UP, NEXT, <-, etc. Separator, // " ' + BY Var, // . , ; : # - UnaryOp, // & v V ? + UnaryOp, // & v V ? | - BinaryOp, // $ ~ Sub, // SUB Word, // any other [a-zA-Z]+ sequence diff --git a/cringe/Tokenizer.cs b/cringe/Tokenizer.cs index 2444f35..b4e10d2 100644 --- a/cringe/Tokenizer.cs +++ b/cringe/Tokenizer.cs @@ -156,7 +156,7 @@ public Token NextToken() pos++; return new Token(TokenType.Var, c.ToString(), startPos); - case '&': case '?': + case '&': case '?': case '|': case '-': pos++; return new Token(TokenType.UnaryOp, c.ToString(), startPos); diff --git a/cringe/perplex.cs b/cringe/perplex.cs index cd5f6a4..345b9ec 100644 --- a/cringe/perplex.cs +++ b/cringe/perplex.cs @@ -113,8 +113,8 @@ public virtual Expression Optimize() class QuotedExpression : Expression { string delimeter; //either ' or " - string unary_op = null; - + List unary_ops = new List(); + Expression child; public QuotedExpression(Scanner s) @@ -122,11 +122,12 @@ public QuotedExpression(Scanner s) delimeter = s.Current.Value; s.MoveNext(); - if(s.Current.Type == TokenType.UnaryOp) + //3.4.3 A unary operator is applied to a sparked or rabbit-eared + //expression by inserting the operator immediately following the opening spark or ears + // Multiple unary operators can be stacked (e.g., '&-|?.1') + while(s.Current.Type == TokenType.UnaryOp) { - //3.4.3 A unary operator is applied to a sparked or rabbit-eared - //expression by inserting the operator immediately following the opening spark or ears - unary_op = s.Current.Value; + unary_ops.Add(s.Current.Value); s.MoveNext(); } @@ -145,6 +146,19 @@ public QuotedExpression(Scanner s) this.returnType = child.ReturnType; } + public static ulong ApplyUnaryOp(string op, ulong val) + { + switch(op) + { + case "v": case "V": return Lib.Or(val); + case "&": return Lib.And(val); + case "?": return Lib.Xor(val); + case "|": return Lib.Mirror(val); + case "-": return Lib.Invert(val); + default: throw new CompilationException("Bad unary operator"); + } + } + public override Expression Optimize() { //first we optimize the child. @@ -156,138 +170,58 @@ public override Expression Optimize() //a constant expression. if(c!= null) { - if(unary_op == null) + if(unary_ops.Count == 0) return child; - else - { - if(c.Value <= UInt16.MaxValue) - { - ushort tmp = (ushort)c.Value; - switch(unary_op) - { - case "v": case "V": - return new ConstantExpression((ulong)Lib.UnaryOr16(tmp)); - case "&": - return new ConstantExpression((ulong)Lib.UnaryAnd16(tmp)); - case "?": - return new ConstantExpression((ulong)Lib.UnaryXor16(tmp)); - } - } - else if(c.Value <= UInt32.MaxValue) - { - switch(unary_op) - { - case "v": case "V": - return new ConstantExpression((ulong)Lib.UnaryOr32((uint)c.Value)); - case "&": - return new ConstantExpression((ulong)Lib.UnaryAnd32((uint)c.Value)); - case "?": - return new ConstantExpression((ulong)Lib.UnaryXor32((uint)c.Value)); - } - } - else - { - switch(unary_op) - { - case "v": case "V": - return new ConstantExpression(Lib.UnaryOr64(c.Value)); - case "&": - return new ConstantExpression(Lib.UnaryAnd64(c.Value)); - case "?": - return new ConstantExpression(Lib.UnaryXor64(c.Value)); - } - } + // Apply operators right-to-left + ulong val = c.Value; + for(int i = unary_ops.Count - 1; i >= 0; i--) + { + val = ApplyUnaryOp(unary_ops[i], val); } + return new ConstantExpression(val); } //if the child expression was not constant then we can't optimize. return this; } + private static string EmitNameForOp(string op) + { + switch(op) + { + case "v": case "V": return "Lib.Or"; + case "&": return "Lib.And"; + case "?": return "Lib.Xor"; + case "|": return "Lib.Mirror"; + case "-": return "Lib.Invert"; + default: throw new CompilationException("Bad unary operator"); + } + } + public override void Emit(CompilationContext ctx) { ctx.EmitRaw("("); - if(this.unary_op == null) + // Emit nested calls left-to-right: &-|x becomes Lib.And(Lib.Invert(Lib.Mirror(x))) + foreach(string op in unary_ops) { - child.Emit(ctx); + ctx.EmitRaw(EmitNameForOp(op) + "("); } - else + child.Emit(ctx); + for(int i = 0; i < unary_ops.Count; i++) { - switch(unary_op) - { - case "v": case "V": - ctx.EmitRaw("Lib.Or("); - child.Emit(ctx); - ctx.EmitRaw(")"); - break; - case "&": - ctx.EmitRaw("Lib.And("); - child.Emit(ctx); - ctx.EmitRaw(")"); - break; - case "?": - ctx.EmitRaw("Lib.Xor("); - child.Emit(ctx); - ctx.EmitRaw(")"); - break; - } + ctx.EmitRaw(")"); } - ctx.EmitRaw(")"); } public override ulong Evaluate(ExecutionContext ctx) { ulong result = child.Evaluate(ctx); - if(unary_op != null) + // Apply operators right-to-left (innermost first) + for(int i = unary_ops.Count - 1; i >= 0; i--) { - if(result <= UInt16.MaxValue) - { - ushort tmp = (ushort)result; - switch(unary_op) - { - case "v": case "V": - result = (ulong)Lib.UnaryOr16(tmp); - break; - case "&": - result = (ulong)Lib.UnaryAnd16(tmp); - break; - case "?": - result = (ulong)Lib.UnaryXor16(tmp); - break; - } - } - else if(result <= UInt32.MaxValue) - { - switch(unary_op) - { - case "v": case "V": - result = (ulong)Lib.UnaryOr32((uint)result); - break; - case "&": - result = (ulong)Lib.UnaryAnd32((uint)result); - break; - case "?": - result = (ulong)Lib.UnaryXor32((uint)result); - break; - } - } - else - { - switch(unary_op) - { - case "v": case "V": - result = Lib.UnaryOr64(result); - break; - case "&": - result = Lib.UnaryAnd64(result); - break; - case "?": - result = Lib.UnaryXor64(result); - break; - } - } + result = ApplyUnaryOp(unary_ops[i], result); } return result; } @@ -306,6 +240,8 @@ static ConstantExpression() eval_table["V"] = new eval_delegate(Lib.UnaryOr16); eval_table["v"] = new eval_delegate(Lib.UnaryOr16); eval_table["?"] = new eval_delegate(Lib.UnaryXor16); + eval_table["|"] = new eval_delegate(Lib.Mirror16); + eval_table["-"] = new eval_delegate(Lib.Invert16); } //This constructor is used during optimization @@ -327,17 +263,18 @@ public ConstantExpression(Scanner s) string prefix = s.Current.Value; s.MoveNext(); - if(s.Current.Type == TokenType.UnaryOp) + // Collect any stacked unary operators (e.g., #-|5) + List ops = new List(); + while(s.Current.Type == TokenType.UnaryOp) { - //For some reason there's an unary operator in front of - //the digits. We just do the conversion here... - string op = s.Current.Value; + ops.Add(s.Current.Value); s.MoveNext(); - Value = ((eval_delegate)eval_table[op])((ushort)UInt64.Parse(Statement.ReadGroupValue(s, "digits"))); } - else + Value = UInt64.Parse(Statement.ReadGroupValue(s, "digits")); + // Apply operators right-to-left + for(int i = ops.Count - 1; i >= 0; i--) { - Value = UInt64.Parse(Statement.ReadGroupValue(s, "digits")); + Value = ((eval_delegate)eval_table[ops[i]])((ushort)Value); } // Validate range based on constant prefix @@ -376,17 +313,9 @@ public override void Emit(CompilationContext ctx) //unary operation on the front. class NumericExpression : Expression { - delegate uint long_op(uint arg); - delegate ushort short_op(ushort arg); - delegate ulong vlong_op(ulong arg); - - string unary_op = null; + List unary_ops = new List(); string lval; - long_op longform; - short_op shortform; - vlong_op vlongform; - public NumericExpression(Scanner s) { string typeid = s.Current.Value; @@ -399,87 +328,68 @@ public NumericExpression(Scanner s) else Debug.Assert(false); - if(s.PeekNext.Type == TokenType.UnaryOp) + // Collect any stacked unary operators (e.g., .-|1) + while(s.PeekNext.Type == TokenType.UnaryOp) { - //Theres an unary operator s.MoveNext(); - unary_op = s.Current.Value; - switch(unary_op) - { - case "v": - case "V": - this.shortform = new short_op(Lib.UnaryOr16); - this.longform = new long_op(Lib.UnaryOr32); - this.vlongform = new vlong_op(Lib.UnaryOr64); - break; - case "?": - this.shortform = new short_op(Lib.UnaryXor16); - this.longform = new long_op(Lib.UnaryXor32); - this.vlongform = new vlong_op(Lib.UnaryXor64); - break; - case "&": - this.shortform = new short_op(Lib.UnaryAnd16); - this.longform = new long_op(Lib.UnaryAnd32); - this.vlongform = new vlong_op(Lib.UnaryAnd64); - break; - } + unary_ops.Add(s.Current.Value); } s.MoveNext(); this.lval = typeid + Statement.ReadGroupValue(s, "digits"); } - public override ulong Evaluate(ExecutionContext ctx) + private static string EmitNameForOp(string op, Type width) { - if(this.longform == null) - return ctx[lval]; + bool is16 = width == typeof(UInt16); + bool is64 = width == typeof(UInt64); + switch(op) + { + case "v": case "V": return is16 ? "Lib.UnaryOr16" : is64 ? "Lib.UnaryOr64" : "Lib.UnaryOr32"; + case "&": return is16 ? "Lib.UnaryAnd16" : is64 ? "Lib.UnaryAnd64" : "Lib.UnaryAnd32"; + case "?": return is16 ? "Lib.UnaryXor16" : is64 ? "Lib.UnaryXor64" : "Lib.UnaryXor32"; + case "|": return is16 ? "Lib.Mirror16" : is64 ? "Lib.Mirror64" : "Lib.Mirror32"; + case "-": return is16 ? "Lib.Invert16" : is64 ? "Lib.Invert64" : "Lib.Invert32"; + default: throw new CompilationException("Bad unary operator"); + } + } - if(this.returnType == typeof(UInt16)) - return shortform((ushort)ctx[lval]); - else if(this.returnType == typeof(UInt64)) - return vlongform(ctx[lval]); - else - return longform((uint)ctx[lval]); + public override ulong Evaluate(ExecutionContext ctx) + { + ulong result = ctx[lval]; + // Apply operators right-to-left + for(int i = unary_ops.Count - 1; i >= 0; i--) + { + result = QuotedExpression.ApplyUnaryOp(unary_ops[i], result); + } + return result; } public override void Emit(CompilationContext ctx) { - if(this.longform == null) + if(unary_ops.Count == 0) + { ctx.EmitRaw("frame.ExecutionContext[\"" + lval + "\"]"); + } else { - string sf = null; - string lf = null; - string vlf = null; - switch(unary_op) + // Emit nested calls: outermost op first + foreach(string op in unary_ops) { - case "v": - case "V": - sf = "Lib.UnaryOr16"; - lf = "Lib.UnaryOr32"; - vlf = "Lib.UnaryOr64"; - break; - case "?": - sf = "Lib.UnaryXor16"; - lf = "Lib.UnaryXor32"; - vlf = "Lib.UnaryXor64"; - break; - case "&": - sf = "Lib.UnaryAnd16"; - lf = "Lib.UnaryAnd32"; - vlf = "Lib.UnaryAnd64"; - break; - default: - throw new CompilationException("Bad unary operator"); - + ctx.EmitRaw(EmitNameForOp(op, this.returnType) + "("); } if(this.returnType == typeof(UInt16)) - ctx.EmitRaw(sf + "(((ushort)frame.ExecutionContext[\"" + lval + "\"]))"); + ctx.EmitRaw("((ushort)frame.ExecutionContext[\"" + lval + "\"])"); else if(this.returnType == typeof(UInt64)) - ctx.EmitRaw(vlf + "(frame.ExecutionContext[\"" + lval + "\"])"); + ctx.EmitRaw("frame.ExecutionContext[\"" + lval + "\"]"); else - ctx.EmitRaw(lf + "(((uint)frame.ExecutionContext[\"" + lval + "\"]))"); + ctx.EmitRaw("((uint)frame.ExecutionContext[\"" + lval + "\"])"); + + for(int i = 0; i < unary_ops.Count; i++) + { + ctx.EmitRaw(")"); + } } } } diff --git a/intercal.runtime/utils.cs b/intercal.runtime/utils.cs index a55837c..f396751 100644 --- a/intercal.runtime/utils.cs +++ b/intercal.runtime/utils.cs @@ -1066,6 +1066,59 @@ public static ulong Xor(ulong val) public static ushort UnaryXor16(ushort val) { return (ushort)(val ^ Rotate(val)); } public static ulong UnaryXor64(ulong val) { return val ^ Rotate(val); } + public static ulong Mirror(ulong val) + { + if (val <= UInt16.MaxValue) + return (ulong)Mirror16((ushort)val); + else if (val <= UInt32.MaxValue) + return (ulong)Mirror32((uint)val); + else + return Mirror64(val); + } + public static ushort Mirror16(ushort val) + { + ushort result = 0; + for (int i = 0; i < 16; i++) + { + if ((val & (1 << i)) != 0) + result |= (ushort)(1 << (15 - i)); + } + return result; + } + public static uint Mirror32(uint val) + { + uint result = 0; + for (int i = 0; i < 32; i++) + { + if ((val & (1U << i)) != 0) + result |= 1U << (31 - i); + } + return result; + } + public static ulong Mirror64(ulong val) + { + ulong result = 0; + for (int i = 0; i < 64; i++) + { + if ((val & (1UL << i)) != 0) + result |= 1UL << (63 - i); + } + return result; + } + + public static ulong Invert(ulong val) + { + if (val <= UInt16.MaxValue) + return (ulong)Invert16((ushort)val); + else if (val <= UInt32.MaxValue) + return (ulong)Invert32((uint)val); + else + return Invert64(val); + } + public static ushort Invert16(ushort val) { return (ushort)~val; } + public static uint Invert32(uint val) { return ~val; } + public static ulong Invert64(ulong val) { return ~val; } + public static int Rand(int n) { diff --git a/intercal.tests/LibTests.cs b/intercal.tests/LibTests.cs index 63e51ef..ac76207 100644 --- a/intercal.tests/LibTests.cs +++ b/intercal.tests/LibTests.cs @@ -298,5 +298,137 @@ public void Fail_IncludesErrorCode() var ex = Assert.Throws(() => Lib.Fail("E999")); Assert.Contains("E999", ex.Message); } + + // Mirror (horizontal mirror / staple) tests + [Fact] + public void Mirror16_One_ReturnsHighBit() + { + Assert.Equal((ushort)0x8000, Lib.Mirror16(1)); + } + + [Fact] + public void Mirror16_HighBit_ReturnsOne() + { + Assert.Equal((ushort)1, Lib.Mirror16(0x8000)); + } + + [Fact] + public void Mirror16_Zero_ReturnsZero() + { + Assert.Equal((ushort)0, Lib.Mirror16(0)); + } + + [Fact] + public void Mirror16_AllOnes_ReturnsAllOnes() + { + Assert.Equal((ushort)0xFFFF, Lib.Mirror16(0xFFFF)); + } + + [Fact] + public void Mirror16_IsOwnInverse() + { + // ||x == x for all values + ushort val = 0x1234; + Assert.Equal(val, Lib.Mirror16(Lib.Mirror16(val))); + } + + [Fact] + public void Mirror32_One_ReturnsHighBit() + { + Assert.Equal(0x80000000u, Lib.Mirror32(1)); + } + + [Fact] + public void Mirror32_IsOwnInverse() + { + uint val = 0xDEADBEEF; + Assert.Equal(val, Lib.Mirror32(Lib.Mirror32(val))); + } + + [Fact] + public void Mirror64_One_ReturnsHighBit() + { + Assert.Equal(0x8000000000000000UL, Lib.Mirror64(1)); + } + + [Fact] + public void Mirror64_IsOwnInverse() + { + ulong val = 0xCAFEBABEDEADBEEFUL; + Assert.Equal(val, Lib.Mirror64(Lib.Mirror64(val))); + } + + [Fact] + public void Mirror_Dispatcher_16bit() + { + // Values <= 16-bit use Mirror16 + Assert.Equal((ulong)0x8000, Lib.Mirror(1)); + } + + [Fact] + public void Mirror_Dispatcher_32bit() + { + // Values > 16-bit use Mirror32. Bit 16 reversed in 32 bits → bit 15 + Assert.Equal((ulong)0x8000, Lib.Mirror(0x10000)); + } + + // Invert (vertical mirror / worm) tests + [Fact] + public void Invert16_Zero_ReturnsAllOnes() + { + Assert.Equal((ushort)0xFFFF, Lib.Invert16(0)); + } + + [Fact] + public void Invert16_AllOnes_ReturnsZero() + { + Assert.Equal((ushort)0, Lib.Invert16(0xFFFF)); + } + + [Fact] + public void Invert16_One_ReturnsComplement() + { + Assert.Equal((ushort)0xFFFE, Lib.Invert16(1)); + } + + [Fact] + public void Invert16_IsOwnInverse() + { + ushort val = 0x1234; + Assert.Equal(val, Lib.Invert16(Lib.Invert16(val))); + } + + [Fact] + public void Invert32_IsOwnInverse() + { + uint val = 0xDEADBEEF; + Assert.Equal(val, Lib.Invert32(Lib.Invert32(val))); + } + + [Fact] + public void Invert64_IsOwnInverse() + { + ulong val = 0xCAFEBABEDEADBEEFUL; + Assert.Equal(val, Lib.Invert64(Lib.Invert64(val))); + } + + // Mirror and Invert commute + [Fact] + public void MirrorInvert_Commute_16bit() + { + ushort val = 0x1234; + Assert.Equal( + Lib.Mirror16(Lib.Invert16(val)), + Lib.Invert16(Lib.Mirror16(val))); + } + + // |-|- is identity (the # revelation) + [Fact] + public void MirrorInvertMirrorInvert_IsIdentity() + { + ushort val = 0xABCD; + ushort result = Lib.Mirror16(Lib.Invert16(Lib.Mirror16(Lib.Invert16(val)))); + Assert.Equal(val, result); + } } } diff --git a/intercal.tests/ProgramTests.cs b/intercal.tests/ProgramTests.cs index f04b103..f1577f3 100644 --- a/intercal.tests/ProgramTests.cs +++ b/intercal.tests/ProgramTests.cs @@ -242,5 +242,87 @@ public void Parse_MingleMixedSpotTwoSpot_Succeeds() "DO GIVE UP\n"); Assert.Equal(3, p.StatementCount); } + + // Mirror operator (|) — horizontal mirror / staple + [Fact] + public void Parse_MirrorConstant() + { + var p = ParseSource( + "DO .1 <- #|1\n" + + "DO GIVE UP\n"); + Assert.Equal(2, p.StatementCount); + } + + [Fact] + public void Parse_MirrorVariable() + { + var p = ParseSource( + "DO .1 <- #5\n" + + "DO .2 <- .|1\n" + + "DO GIVE UP\n"); + Assert.Equal(3, p.StatementCount); + } + + [Fact] + public void Parse_MirrorQuotedExpression() + { + var p = ParseSource( + "DO .1 <- #5\n" + + "DO .2 <- '|.1'\n" + + "DO GIVE UP\n"); + Assert.Equal(3, p.StatementCount); + } + + // Invert operator (-) — vertical mirror / worm + [Fact] + public void Parse_InvertConstant() + { + var p = ParseSource( + "DO .1 <- #-1\n" + + "DO GIVE UP\n"); + Assert.Equal(2, p.StatementCount); + } + + [Fact] + public void Parse_InvertVariable() + { + var p = ParseSource( + "DO .1 <- #5\n" + + "DO .2 <- .-1\n" + + "DO GIVE UP\n"); + Assert.Equal(3, p.StatementCount); + } + + // Stacked unary operators + [Fact] + public void Parse_StackedMirrorInvert_Constant() + { + // #-|5 means: mirror 5, then invert + var p = ParseSource( + "DO .1 <- #-|5\n" + + "DO GIVE UP\n"); + Assert.Equal(2, p.StatementCount); + } + + [Fact] + public void Parse_StackedMirrorInvert_QuotedExpression() + { + // '-|.1' means: mirror .1, then invert + var p = ParseSource( + "DO .1 <- #5\n" + + "DO .2 <- '-|.1'\n" + + "DO GIVE UP\n"); + Assert.Equal(3, p.StatementCount); + } + + [Fact] + public void Parse_IdentityStack_MirrorInvertMirrorInvert() + { + // |-|- is identity (the # revelation) + var p = ParseSource( + "DO .1 <- #|-|-5\n" + + "DO GIVE UP\n"); + Assert.Equal(2, p.StatementCount); + } } } diff --git a/intercal.tests/ScannerTests.cs b/intercal.tests/ScannerTests.cs index e5c76d7..e6a50ca 100644 --- a/intercal.tests/ScannerTests.cs +++ b/intercal.tests/ScannerTests.cs @@ -352,5 +352,37 @@ public void Tokenizer_CompleteStatement() Assert.Equal(TokenType.Digits, scanner.Current.Type); Assert.Equal("42", scanner.Current.Value); } + + // Mirror (|) and Invert (-) tokenization + [Fact] + public void Tokenizer_PipeIsUnaryOp() + { + var scanner = Scanner.CreateScanner("|"); + Assert.Equal(TokenType.UnaryOp, scanner.Current.Type); + Assert.Equal("|", scanner.Current.Value); + } + + [Fact] + public void Tokenizer_DashIsUnaryOp() + { + var scanner = Scanner.CreateScanner("-"); + Assert.Equal(TokenType.UnaryOp, scanner.Current.Type); + Assert.Equal("-", scanner.Current.Value); + } + + [Fact] + public void Tokenizer_StackedUnaryOps() + { + // -|? should tokenize as three consecutive UnaryOp tokens + var scanner = Scanner.CreateScanner("-|?"); + Assert.Equal(TokenType.UnaryOp, scanner.Current.Type); + Assert.Equal("-", scanner.Current.Value); + scanner.MoveNext(); + Assert.Equal(TokenType.UnaryOp, scanner.Current.Type); + Assert.Equal("|", scanner.Current.Value); + scanner.MoveNext(); + Assert.Equal(TokenType.UnaryOp, scanner.Current.Type); + Assert.Equal("?", scanner.Current.Value); + } } }