diff --git a/README.md b/README.md index ccdce5b..6d31bda 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ Increments: - [X] Integers - [X] Binary Operators on Ints -- [ ] Unary Operators on Ints -- [ ] Variables and Assignment +- [X] Unary Operators on Ints +- [ ] Local Variables and Assignment - [ ] Booleans and Comparisons - [ ] WriteLn("Hello World") (finally) - [ ] Branching (if statements) @@ -24,3 +24,25 @@ Increments: - [ ] Arrays - [ ] Records - [ ] Pointers + +## Testing + +HSPC's approach to testing is to test parity with [fpc](https://www.freepascal.org/). +This is tested with integration tests comparing the `STDOUT` and +`EXIT_CODE` of compiled versions of the same pascal code +(one by `hspc` and one by `fpc`). + +To rerun the programs against `fpc`. + +```bash +# from project root +cd test/sample_programs +bash generate_goldens.bash +``` + +To run the tests. + +```bash +# from project root +cabal test +``` diff --git a/src/HSPC/CodeGen.hs b/src/HSPC/CodeGen.hs index ee5fd92..275b957 100644 --- a/src/HSPC/CodeGen.hs +++ b/src/HSPC/CodeGen.hs @@ -20,8 +20,12 @@ generateProgram (Program _ body) = concatMap generateProgram body generateProgram (Block body) = concatMap generateProgram body generateProgram (Halt ast) = generateProgram ast - ++ [0x48, 0x89, 0xc7] -- Copy rax to rdi - ++ [0xb8, 0x3c, 00, 00, 00] -- mov rax 60 (sys_exit number) + -- copy rax -> rdi but maxed out at 255 + ++ [0xbf, 0xff, 0, 0, 0] -- mov edi 255 + ++ [0x48, 0x3d, 0xff, 0, 0, 0] -- cmp rax 255 + ++ [0x48, 0x0f, 0x4c, 0xf8] -- cmovl rax rdi + -- exit_group syscall, exit code is in rdi + ++ [0xb8, 0x3c, 00, 00, 00] -- mov rax 60 ++ [0x0f, 0x05] -- Syscall generateProgram (IntLiteral i) = [0x48, 0xb8] ++ int64ToLE i -- movabs rax i @@ -37,6 +41,10 @@ generateProgram (Multiply op1 op2) = ++ generateProgram op2 ++ [0x48, 0x0f, 0xaf, 0x04, 0x24] -- imul rax [rsp] ++ [0x48, 0x83, 0xc4, 0x08] -- add rsp 8 (release stack space) +generateProgram (UnaryPlus op) = generateProgram op -- unary + is a nop +generateProgram (UnaryMinus op) = + generateProgram op + ++ [0x48, 0xf7, 0xd8] generateProgram (IntDivide op1 op2) = generateProgram op2 ++ [0x50] -- push rax @@ -44,4 +52,4 @@ generateProgram (IntDivide op1 op2) = ++ [0x48, 0x99] -- cqo ++ [0x48, 0xf7, 0x3c, 0x24] -- idiv qword [rsp] ++ [0x48, 0x83, 0xc4, 0x08] -- add rsp 8 (release stack space) -generateProgram ast = error $ "CodeGen for " ++ show ast ++ "Not implemented" +generateProgram ast = error $ "CodeGen for " ++ show ast ++ " not implemented" diff --git a/src/HSPC/Parse.hs b/src/HSPC/Parse.hs index ef175f5..5699797 100644 --- a/src/HSPC/Parse.hs +++ b/src/HSPC/Parse.hs @@ -8,6 +8,8 @@ data AST | Block [AST] | Identifier String | IntLiteral Int64 + | UnaryPlus AST + | UnaryMinus AST | Add AST AST | IntDivide AST AST | Subtract AST AST @@ -20,7 +22,7 @@ data ParseError | ExpectedEndStatement | BlockMustStartWithBegin | BracketNotClosed - | ExpectedOperand + | ExpectedOperand String | UnmatchedBracket | UnexpectedToken HSPCToken | ExpectedToken HSPCToken @@ -88,17 +90,20 @@ parseExpr tok = Left $ NotImplemented tok -- IntOperand = Term { ("+" | "-") Term } -- Term = Factor { ("*" | "div") Factor } --- Factor = Number | "(" Expression ")" +-- Factor = ("+" | "-") Factor | Number | "(" IntOperand ")" -- Number = // an integer parseIntOperand :: [HSPCToken] -> Either ParseError AST parseIntOperand toks = case breakLastOuter (`elem` [PlusTok, MinusTok]) toks of + -- starts with +- + ([], after) -> parseTerm after + -- doesn't contain +- (before, []) -> parseTerm before - (_, [PlusTok]) -> Left ExpectedOperand + (_, [PlusTok]) -> Left $ ExpectedOperand "Expected operand after +" (before, PlusTok : after) -> do left <- parseIntOperand before right <- parseTerm after return $ Add left right - (_, [MinusTok]) -> Left ExpectedOperand + (_, [MinusTok]) -> Left $ ExpectedOperand "Expected operand after -" (before, MinusTok : after) -> do left <- parseIntOperand before right <- parseTerm after @@ -108,12 +113,12 @@ parseIntOperand toks = case breakLastOuter (`elem` [PlusTok, MinusTok]) toks of parseTerm :: [HSPCToken] -> Either ParseError AST parseTerm toks = case breakLastOuter (`elem` [IntDivideTok, MultiplyTok]) toks of (before, []) -> parseFactor before - (_, [IntDivideTok]) -> Left ExpectedOperand + (_, [IntDivideTok]) -> Left $ ExpectedOperand "Expected operand after 'div'" (before, IntDivideTok : after) -> do left <- parseIntOperand before right <- parseFactor after return $ IntDivide left right - (_, [MultiplyTok]) -> Left ExpectedOperand + (_, [MultiplyTok]) -> Left $ ExpectedOperand "Expected operand after *" (before, MultiplyTok : after) -> do left <- parseIntOperand before right <- parseFactor after @@ -122,10 +127,16 @@ parseTerm toks = case breakLastOuter (`elem` [IntDivideTok, MultiplyTok]) toks o parseFactor :: [HSPCToken] -> Either ParseError AST parseFactor [LiteralIntTok n] = Right $ IntLiteral n +parseFactor (PlusTok : xs) = do + operand <- parseFactor xs + return $ UnaryPlus operand +parseFactor (MinusTok : xs) = do + operand <- parseFactor xs + return $ UnaryMinus operand parseFactor (OpenBracketTok : xs) = do (inside, remaining) <- splitToMatchingBracket xs case remaining of [] -> parseIntOperand inside (tok : _) -> Left $ UnexpectedToken tok parseFactor (tok : _) = Left $ UnexpectedToken tok -parseFactor _ = Left ExpectedOperand +parseFactor [] = Left $ ExpectedOperand "Expected operand" diff --git a/test/Main.hs b/test/Main.hs index 42f0554..a207c44 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -12,8 +12,9 @@ import Test.Tasty.HUnit data TestSpec = TestSpec { name :: String, - srcPath :: FilePath, - goldenPath :: FilePath, + -- There should be .pas and .golden + -- in sampleProgramsLocation + testName :: FilePath, expectedExit :: Int } @@ -22,13 +23,16 @@ sampleProgramsLocation = "test/sample_programs/" testSuite :: [TestSpec] testSuite = - [ TestSpec "Exit code 0" "ints_0_exit_code.pas" "/dev/null" 0, - TestSpec "Exit code 255" "ints_255_exit_code.pas" "/dev/null" 255, - TestSpec "Halt after Halt" "ints_halt_after_halt.pas" "/dev/null" 0, - TestSpec "Add literal ints" "operators_add_two_ints.pas" "/dev/null" 200, - TestSpec "Multiply literal ints" "operators_multiply_two_ints.pas" "/dev/null" 40, - TestSpec "Divide(div) literal ints" "operators_divide_two_ints.pas" "/dev/null" 2, - TestSpec "Brackets with literal ints" "operators_brackets.pas" "/dev/null" 26 + [ TestSpec "Exit code 0" "ints_0_exit_code" 0, + TestSpec "Exit code 255" "ints_255_exit_code" 255, + TestSpec "Exit code > 255" "ints_big_exit_code" 255, + TestSpec "Halt after Halt" "ints_halt_after_halt" 0, + TestSpec "Add literal ints" "operators_add_two_ints" 200, + TestSpec "Multiply literal ints" "operators_multiply_two_ints" 40, + TestSpec "Divide(div) literal ints" "operators_divide_two_ints" 2, + TestSpec "Brackets with literal ints" "operators_brackets" 26, + TestSpec "Unary minus with literal ints" "operators_unary_minus_int" 75, + TestSpec "Unary plus with literal ints" "operators_unary_plus_int" 25 ] main :: IO () @@ -40,7 +44,7 @@ main = runIntegrationTest :: TestSpec -> Assertion runIntegrationTest spec = do - source <- readFile (sampleProgramsLocation ++ srcPath spec) + source <- readFile (sampleProgramsLocation ++ testName spec ++ ".pas") case compile source of Left err -> assertFailure $ "Compilation failed: " ++ show err @@ -56,12 +60,12 @@ runIntegrationTest spec = do ExitFailure n -> n assertEqual - ("Exit code mismatch for " ++ srcPath spec ++ "\nStderr: " ++ stderr) + ("Exit code mismatch for " ++ testName spec ++ "\nStderr: " ++ stderr) (expectedExit spec) actualCode - goldenOut <- readFile (goldenPath spec) + goldenOut <- readFile (sampleProgramsLocation ++ testName spec ++ ".golden") assertEqual - ("StdOut mismatch for " ++ srcPath spec ++ "\nStderr: " ++ stderr) + ("StdOut mismatch for " ++ testName spec ++ "\nStderr: " ++ stderr) goldenOut stdout diff --git a/test/sample_programs/generate_goldens.bash b/test/sample_programs/generate_goldens.bash new file mode 100644 index 0000000..1d378b1 --- /dev/null +++ b/test/sample_programs/generate_goldens.bash @@ -0,0 +1,47 @@ +#!/bin/bash +# Note: AI generated by gemini + +# Define formatting for the table output +# %-30s means left-align string with 30 chars width +FORMAT="%-30s | %-10s\n" + +# Print Table Header +printf "$FORMAT" "Test Name" "Exit Code" +echo "-------------------------------+-----------" + +# Loop through all .pas files in the current directory +for source_file in *.pas; do + + # Handle case where no .pas files exist + [ -e "$source_file" ] || continue + + # Extract the filename without the extension (e.g., test1.pas -> test1) + test_name="${source_file%.*}" + + # 1. Compile with Free Pascal (fpc) + # -v0 suppresses the fpc logo and verbose messages to keep stdout clean + # We redirect compiler output to /dev/null so it doesn't clutter the table + if fpc -v0 "$source_file" > /dev/null 2>&1; then + + # 2. Run the executable + # Redirect stdout to the golden file + # Stderr is left visible (or you can redirect it to /dev/null) + ./"$test_name" > "${test_name}.golden" + + # 3. Capture the exit code of the executable + exit_code=$? + + # 4. Print the row in the table + printf "$FORMAT" "$test_name" "$exit_code" + + # 5. Cleanup (Optional) + # fpc creates an executable and an object file (.o). + # We remove them to keep the directory clean. + rm -f "$test_name" "${test_name}.o" + + else + # If fpc failed to compile + printf "$FORMAT" "$test_name" "COMPILE ERR" + fi + +done diff --git a/test/sample_programs/ints_0_exit_code.golden b/test/sample_programs/ints_0_exit_code.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/ints_255_exit_code.golden b/test/sample_programs/ints_255_exit_code.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/ints_big_exit_code.golden b/test/sample_programs/ints_big_exit_code.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/ints_big_exit_code.pas b/test/sample_programs/ints_big_exit_code.pas new file mode 100644 index 0000000..0d4b0cd --- /dev/null +++ b/test/sample_programs/ints_big_exit_code.pas @@ -0,0 +1,5 @@ +program HaltExample; + +begin + halt(10000); +end. diff --git a/test/sample_programs/ints_halt_after_halt.golden b/test/sample_programs/ints_halt_after_halt.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/operators_add_two_ints.golden b/test/sample_programs/operators_add_two_ints.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/operators_brackets.golden b/test/sample_programs/operators_brackets.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/operators_divide_two_ints.golden b/test/sample_programs/operators_divide_two_ints.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/operators_multiply_two_ints.golden b/test/sample_programs/operators_multiply_two_ints.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/operators_unary_minus_int.golden b/test/sample_programs/operators_unary_minus_int.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/operators_unary_minus_int.pas b/test/sample_programs/operators_unary_minus_int.pas new file mode 100644 index 0000000..6df4af2 --- /dev/null +++ b/test/sample_programs/operators_unary_minus_int.pas @@ -0,0 +1,5 @@ +program UnaryMinusTest; + +begin + halt(100+(-25)); +end. diff --git a/test/sample_programs/operators_unary_plus_int.golden b/test/sample_programs/operators_unary_plus_int.golden new file mode 100644 index 0000000..e69de29 diff --git a/test/sample_programs/operators_unary_plus_int.pas b/test/sample_programs/operators_unary_plus_int.pas new file mode 100644 index 0000000..cc26d2f --- /dev/null +++ b/test/sample_programs/operators_unary_plus_int.pas @@ -0,0 +1,5 @@ +program UnaryPlusTest; + +begin + halt(+(-(-25))); +end.