Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
```
14 changes: 11 additions & 3 deletions src/HSPC/CodeGen.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,11 +41,15 @@ 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
++ generateProgram op1
++ [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"
25 changes: 18 additions & 7 deletions src/HSPC/Parse.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,7 +22,7 @@ data ParseError
| ExpectedEndStatement
| BlockMustStartWithBegin
| BracketNotClosed
| ExpectedOperand
| ExpectedOperand String
| UnmatchedBracket
| UnexpectedToken HSPCToken
| ExpectedToken HSPCToken
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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"
30 changes: 17 additions & 13 deletions test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import Test.Tasty.HUnit

data TestSpec = TestSpec
{ name :: String,
srcPath :: FilePath,
goldenPath :: FilePath,
-- There should be <testName>.pas and <testName>.golden
-- in sampleProgramsLocation
testName :: FilePath,
expectedExit :: Int
}

Expand All @@ -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 ()
Expand All @@ -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
Expand All @@ -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
47 changes: 47 additions & 0 deletions test/sample_programs/generate_goldens.bash
Original file line number Diff line number Diff line change
@@ -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
Empty file.
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions test/sample_programs/ints_big_exit_code.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
program HaltExample;

begin
halt(10000);
end.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions test/sample_programs/operators_unary_minus_int.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
program UnaryMinusTest;

begin
halt(100+(-25));
end.
Empty file.
5 changes: 5 additions & 0 deletions test/sample_programs/operators_unary_plus_int.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
program UnaryPlusTest;

begin
halt(+(-(-25)));
end.
Loading