diff --git a/IGCSE_Compiler_Tests.postman_collection.json b/IGCSE_Compiler_Tests.postman_collection.json new file mode 100644 index 0000000..52dddc8 --- /dev/null +++ b/IGCSE_Compiler_Tests.postman_collection.json @@ -0,0 +1,628 @@ +{ + "info": { + "name": "IGCSE Pseudocode Compiler", + "description": "Test collection for IGCSE Pseudocode Compiler API with 30 comprehensive examples", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "1. Basics", + "item": [ + { + "name": "Hello World", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"OUTPUT \\\"Hello, World!\\\"\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Variables and Assignment", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE x : INTEGER\\nDECLARE y : INTEGER\\nx = 10\\ny = 20\\nOUTPUT \\\"x = \\\", x\\nOUTPUT \\\"y = \\\", y\\nOUTPUT \\\"x + y = \\\", x + y\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Constants", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"CONSTANT PI = 3.14159\\nDECLARE radius : REAL\\nradius = 5\\narea = PI * radius * radius\\nOUTPUT \\\"Area of circle: \\\", area\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + } + ] + }, + { + "name": "2. Input/Output", + "item": [ + { + "name": "Simple Input", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE name : STRING\\nINPUT name\\nOUTPUT \\\"Hello, \\\", name\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Multiple Inputs", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE length : REAL\\nDECLARE width : REAL\\nOUTPUT \\\"Enter length: \\\"\\nINPUT length\\nOUTPUT \\\"Enter width: \\\"\\nINPUT width\\narea = length * width\\nOUTPUT \\\"Area = \\\", area\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + } + ] + }, + { + "name": "3. Conditionals", + "item": [ + { + "name": "IF Statement", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE age : INTEGER\\nINPUT age\\nIF age >= 18 THEN\\n OUTPUT \\\"You are an adult\\\"\\nELSE\\n OUTPUT \\\"You are a minor\\\"\\nENDIF\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Nested IF (Grade Calculator)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE score : INTEGER\\nINPUT score\\nIF score >= 90 THEN\\n OUTPUT \\\"Grade: A\\\"\\nELSEIF score >= 80 THEN\\n OUTPUT \\\"Grade: B\\\"\\nELSEIF score >= 70 THEN\\n OUTPUT \\\"Grade: C\\\"\\nELSEIF score >= 60 THEN\\n OUTPUT \\\"Grade: D\\\"\\nELSE\\n OUTPUT \\\"Grade: F\\\"\\nENDIF\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + } + ] + }, + { + "name": "4. Loops", + "item": [ + { + "name": "FOR Loop", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE i : INTEGER\\nFOR i = 1 TO 10\\n OUTPUT i\\nNEXT i\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "FOR Loop with STEP", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE i : INTEGER\\nFOR i = 0 TO 20 STEP 2\\n OUTPUT i\\nNEXT i\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "WHILE Loop", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE count : INTEGER\\ncount = 1\\nWHILE count <= 5 DO\\n OUTPUT count\\n count = count + 1\\nENDWHILE\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "REPEAT Loop", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE num : INTEGER\\nnum = 1\\nREPEAT\\n OUTPUT num\\n num = num + 1\\nUNTIL num > 5\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Nested Loops", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE i : INTEGER\\nDECLARE j : INTEGER\\nFOR i = 1 TO 3\\n FOR j = 1 TO 3\\n OUTPUT i, \\\" x \\\", j, \\\" = \\\", i * j\\n NEXT j\\nNEXT i\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + } + ] + }, + { + "name": "5. Arrays", + "item": [ + { + "name": "1D Array", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE numbers : ARRAY[1:5] OF INTEGER\\nDECLARE i : INTEGER\\nFOR i = 1 TO 5\\n numbers[i] = i * 2\\nNEXT i\\nFOR i = 1 TO 5\\n OUTPUT numbers[i]\\nNEXT i\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "2D Array (Matrix)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE matrix : ARRAY[1:3, 1:3] OF INTEGER\\nDECLARE i : INTEGER\\nDECLARE j : INTEGER\\nFOR i = 1 TO 3\\n FOR j = 1 TO 3\\n matrix[i, j] = i * j\\n NEXT j\\nNEXT i\\nFOR i = 1 TO 3\\n FOR j = 1 TO 3\\n OUTPUT matrix[i, j], \\\" \\\"\\n NEXT j\\n OUTPUT \\\"\\\"\\nNEXT i\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Find Maximum in Array", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE numbers : ARRAY[1:5] OF INTEGER\\nDECLARE i : INTEGER\\nDECLARE max : INTEGER\\nFOR i = 1 TO 5\\n INPUT numbers[i]\\nNEXT i\\nmax = numbers[1]\\nFOR i = 2 TO 5\\n IF numbers[i] > max THEN\\n max = numbers[i]\\n ENDIF\\nNEXT i\\nOUTPUT \\\"Maximum: \\\", max\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + } + ] + }, + { + "name": "6. Procedures", + "item": [ + { + "name": "Simple Procedure", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"PROCEDURE Greet()\\n OUTPUT \\\"Hello from procedure!\\\"\\nENDPROCEDURE\\n\\nCALL Greet()\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Procedure with Parameters", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"PROCEDURE PrintSum(a : INTEGER, b : INTEGER)\\n DECLARE sum : INTEGER\\n sum = a + b\\n OUTPUT \\\"Sum = \\\", sum\\nENDPROCEDURE\\n\\nCALL PrintSum(5, 3)\\nCALL PrintSum(10, 20)\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Procedure with BYREF (Swap)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"PROCEDURE Swap(BYREF a : INTEGER, BYREF b : INTEGER)\\n DECLARE temp : INTEGER\\n temp = a\\n a = b\\n b = temp\\nENDPROCEDURE\\n\\nDECLARE x : INTEGER\\nDECLARE y : INTEGER\\nx = 5\\ny = 10\\nOUTPUT \\\"Before: x=\\\", x, \\\", y=\\\", y\\nCALL Swap(x, y)\\nOUTPUT \\\"After: x=\\\", x, \\\", y=\\\", y\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + } + ] + }, + { + "name": "7. Functions", + "item": [ + { + "name": "Simple Function (Square)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"FUNCTION Square(n : INTEGER) RETURNS INTEGER\\n RETURN n * n\\nENDFUNCTION\\n\\nDECLARE result : INTEGER\\nresult = Square(5)\\nOUTPUT \\\"5 squared = \\\", result\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Function with Multiple Parameters", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"FUNCTION Add(a : INTEGER, b : INTEGER) RETURNS INTEGER\\n RETURN a + b\\nENDFUNCTION\\n\\nDECLARE sum : INTEGER\\nsum = Add(10, 20)\\nOUTPUT \\\"Sum = \\\", sum\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Factorial Function", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"FUNCTION Factorial(n : INTEGER) RETURNS INTEGER\\n DECLARE result : INTEGER\\n DECLARE i : INTEGER\\n result = 1\\n FOR i = 1 TO n\\n result = result * i\\n NEXT i\\n RETURN result\\nENDFUNCTION\\n\\nDECLARE num : INTEGER\\nDECLARE fact : INTEGER\\nnum = 5\\nfact = Factorial(num)\\nOUTPUT num, \\\"! = \\\", fact\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Is Prime Function", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"FUNCTION IsPrime(n : INTEGER) RETURNS BOOLEAN\\n DECLARE i : INTEGER\\n IF n <= 1 THEN\\n RETURN FALSE\\n ENDIF\\n FOR i = 2 TO n - 1\\n IF n MOD i = 0 THEN\\n RETURN FALSE\\n ENDIF\\n NEXT i\\n RETURN TRUE\\nENDFUNCTION\\n\\nDECLARE num : INTEGER\\nnum = 17\\nIF IsPrime(num) THEN\\n OUTPUT num, \\\" is prime\\\"\\nELSE\\n OUTPUT num, \\\" is not prime\\\"\\nENDIF\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + } + ] + }, + { + "name": "8. Strings", + "item": [ + { + "name": "String Operations", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE text : STRING\\ntext = \\\"Hello\\\"\\nOUTPUT \\\"Length: \\\", LENGTH(text)\\nOUTPUT \\\"Substring: \\\", SUBSTRING(text, 1, 3)\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "String Concatenation", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE first : STRING\\nDECLARE last : STRING\\nDECLARE full : STRING\\nfirst = \\\"John\\\"\\nlast = \\\"Doe\\\"\\nfull = first + \\\" \\\" + last\\nOUTPUT \\\"Full name: \\\", full\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + } + ] + }, + { + "name": "9. Complete Programs", + "item": [ + { + "name": "Calculate Average", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE numbers : ARRAY[1:5] OF INTEGER\\nDECLARE i : INTEGER\\nDECLARE sum : INTEGER\\nDECLARE average : REAL\\nsum = 0\\nFOR i = 1 TO 5\\n OUTPUT \\\"Enter number \\\", i, \\\": \\\"\\n INPUT numbers[i]\\n sum = sum + numbers[i]\\nNEXT i\\naverage = sum / 5\\nOUTPUT \\\"Average = \\\", average\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Number Guessing Game", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE secret : INTEGER\\nDECLARE guess : INTEGER\\nDECLARE attempts : INTEGER\\nsecret = 42\\nattempts = 0\\nOUTPUT \\\"Guess the number (1-100)!\\\"\\nREPEAT\\n INPUT guess\\n attempts = attempts + 1\\n IF guess < secret THEN\\n OUTPUT \\\"Too low!\\\"\\n ELSEIF guess > secret THEN\\n OUTPUT \\\"Too high!\\\"\\n ENDIF\\nUNTIL guess = secret\\nOUTPUT \\\"Correct! You got it in \\\", attempts, \\\" attempts\\\"\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + }, + { + "name": "Bubble Sort", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"pseudocode\": \"DECLARE arr : ARRAY[1:5] OF INTEGER\\nDECLARE i : INTEGER\\nDECLARE j : INTEGER\\nDECLARE temp : INTEGER\\nDECLARE swapped : BOOLEAN\\n\\nOUTPUT \\\"Enter 5 numbers:\\\"\\nFOR i = 1 TO 5\\n INPUT arr[i]\\nNEXT i\\n\\nFOR i = 1 TO 4\\n swapped = FALSE\\n FOR j = 1 TO 5 - i\\n IF arr[j] > arr[j + 1] THEN\\n temp = arr[j]\\n arr[j] = arr[j + 1]\\n arr[j + 1] = temp\\n swapped = TRUE\\n ENDIF\\n NEXT j\\n IF NOT swapped THEN\\n i = 5\\n ENDIF\\nNEXT i\\n\\nOUTPUT \\\"Sorted array:\\\"\\nFOR i = 1 TO 5\\n OUTPUT arr[i]\\nNEXT i\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/execution/convert/", + "host": ["{{baseUrl}}"], + "path": ["execution", "convert", ""] + } + } + } + ] + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8000", + "type": "string" + } + ] +} diff --git a/POSTMAN_REQUESTS.md b/POSTMAN_REQUESTS.md new file mode 100644 index 0000000..e7ee349 --- /dev/null +++ b/POSTMAN_REQUESTS.md @@ -0,0 +1,325 @@ +# IGCSE Pseudocode Compiler - Postman Test Requests + +## API Endpoint +**Base URL**: `http://localhost:8000` (adjust port if needed) +**Endpoint**: `POST /execution/convert/` +**Content-Type**: `application/json` + +## Request Format +```json +{ + "pseudocode": "YOUR_PSEUDOCODE_HERE" +} +``` + +## Response Formats + +### Success Response (200 OK) +```json +{ + "success": true, + "python_code": "# Generated Python code here..." +} +``` + +### Error Response (400 Bad Request) +```json +{ + "success": false, + "error": "Error message", + "suggestions": ["suggestion1", "suggestion2"], + "line": 5, + "column": 10 +} +``` + +--- + +## Test Examples + +### 1. Hello World (Basic) +```json +{ + "pseudocode": "OUTPUT \"Hello, World!\"" +} +``` + +### 2. Variables and Assignment +```json +{ + "pseudocode": "DECLARE x : INTEGER\nDECLARE y : INTEGER\nx = 10\ny = 20\nOUTPUT \"x = \", x\nOUTPUT \"y = \", y\nOUTPUT \"x + y = \", x + y" +} +``` + +### 3. Constants +```json +{ + "pseudocode": "CONSTANT PI = 3.14159\nDECLARE radius : REAL\nradius = 5\narea = PI * radius * radius\nOUTPUT \"Area of circle: \", area" +} +``` + +### 4. Simple Input +```json +{ + "pseudocode": "DECLARE name : STRING\nINPUT name\nOUTPUT \"Hello, \", name" +} +``` + +### 5. Multiple Inputs +```json +{ + "pseudocode": "DECLARE length : REAL\nDECLARE width : REAL\nOUTPUT \"Enter length: \"\nINPUT length\nOUTPUT \"Enter width: \"\nINPUT width\narea = length * width\nOUTPUT \"Area = \", area" +} +``` + +### 6. IF Statement +```json +{ + "pseudocode": "DECLARE age : INTEGER\nINPUT age\nIF age >= 18 THEN\n OUTPUT \"You are an adult\"\nELSE\n OUTPUT \"You are a minor\"\nENDIF" +} +``` + +### 7. Nested IF (Grade Calculator) +```json +{ + "pseudocode": "DECLARE score : INTEGER\nINPUT score\nIF score >= 90 THEN\n OUTPUT \"Grade: A\"\nELSEIF score >= 80 THEN\n OUTPUT \"Grade: B\"\nELSEIF score >= 70 THEN\n OUTPUT \"Grade: C\"\nELSEIF score >= 60 THEN\n OUTPUT \"Grade: D\"\nELSE\n OUTPUT \"Grade: F\"\nENDIF" +} +``` + +### 8. FOR Loop +```json +{ + "pseudocode": "DECLARE i : INTEGER\nFOR i = 1 TO 10\n OUTPUT i\nNEXT i" +} +``` + +### 9. FOR Loop with STEP +```json +{ + "pseudocode": "DECLARE i : INTEGER\nFOR i = 0 TO 20 STEP 2\n OUTPUT i\nNEXT i" +} +``` + +### 10. WHILE Loop +```json +{ + "pseudocode": "DECLARE count : INTEGER\ncount = 1\nWHILE count <= 5 DO\n OUTPUT count\n count = count + 1\nENDWHILE" +} +``` + +### 11. REPEAT Loop +```json +{ + "pseudocode": "DECLARE num : INTEGER\nnum = 1\nREPEAT\n OUTPUT num\n num = num + 1\nUNTIL num > 5" +} +``` + +### 12. Nested Loops (Multiplication Table) +```json +{ + "pseudocode": "DECLARE i : INTEGER\nDECLARE j : INTEGER\nFOR i = 1 TO 3\n FOR j = 1 TO 3\n OUTPUT i, \" x \", j, \" = \", i * j\n NEXT j\nNEXT i" +} +``` + +### 13. 1D Array +```json +{ + "pseudocode": "DECLARE numbers : ARRAY[1:5] OF INTEGER\nDECLARE i : INTEGER\nFOR i = 1 TO 5\n numbers[i] = i * 2\nNEXT i\nFOR i = 1 TO 5\n OUTPUT numbers[i]\nNEXT i" +} +``` + +### 14. 2D Array (Matrix) +```json +{ + "pseudocode": "DECLARE matrix : ARRAY[1:3, 1:3] OF INTEGER\nDECLARE i : INTEGER\nDECLARE j : INTEGER\nFOR i = 1 TO 3\n FOR j = 1 TO 3\n matrix[i, j] = i * j\n NEXT j\nNEXT i\nFOR i = 1 TO 3\n FOR j = 1 TO 3\n OUTPUT matrix[i, j], \" \"\n NEXT j\n OUTPUT \"\"\nNEXT i" +} +``` + +### 15. Find Maximum in Array +```json +{ + "pseudocode": "DECLARE numbers : ARRAY[1:5] OF INTEGER\nDECLARE i : INTEGER\nDECLARE max : INTEGER\nFOR i = 1 TO 5\n INPUT numbers[i]\nNEXT i\nmax = numbers[1]\nFOR i = 2 TO 5\n IF numbers[i] > max THEN\n max = numbers[i]\n ENDIF\nNEXT i\nOUTPUT \"Maximum: \", max" +} +``` + +### 16. Simple Procedure +```json +{ + "pseudocode": "PROCEDURE Greet()\n OUTPUT \"Hello from procedure!\"\nENDPROCEDURE\n\nCALL Greet()" +} +``` + +### 17. Procedure with Parameters +```json +{ + "pseudocode": "PROCEDURE PrintSum(a : INTEGER, b : INTEGER)\n DECLARE sum : INTEGER\n sum = a + b\n OUTPUT \"Sum = \", sum\nENDPROCEDURE\n\nCALL PrintSum(5, 3)\nCALL PrintSum(10, 20)" +} +``` + +### 18. Procedure with BYREF (Swap) +```json +{ + "pseudocode": "PROCEDURE Swap(BYREF a : INTEGER, BYREF b : INTEGER)\n DECLARE temp : INTEGER\n temp = a\n a = b\n b = temp\nENDPROCEDURE\n\nDECLARE x : INTEGER\nDECLARE y : INTEGER\nx = 5\ny = 10\nOUTPUT \"Before: x=\", x, \", y=\", y\nCALL Swap(x, y)\nOUTPUT \"After: x=\", x, \", y=\", y" +} +``` + +### 19. Simple Function +```json +{ + "pseudocode": "FUNCTION Square(n : INTEGER) RETURNS INTEGER\n RETURN n * n\nENDFUNCTION\n\nDECLARE result : INTEGER\nresult = Square(5)\nOUTPUT \"5 squared = \", result" +} +``` + +### 20. Function with Multiple Parameters +```json +{ + "pseudocode": "FUNCTION Add(a : INTEGER, b : INTEGER) RETURNS INTEGER\n RETURN a + b\nENDFUNCTION\n\nDECLARE sum : INTEGER\nsum = Add(10, 20)\nOUTPUT \"Sum = \", sum" +} +``` + +### 21. Factorial Function +```json +{ + "pseudocode": "FUNCTION Factorial(n : INTEGER) RETURNS INTEGER\n DECLARE result : INTEGER\n DECLARE i : INTEGER\n result = 1\n FOR i = 1 TO n\n result = result * i\n NEXT i\n RETURN result\nENDFUNCTION\n\nDECLARE num : INTEGER\nDECLARE fact : INTEGER\nnum = 5\nfact = Factorial(num)\nOUTPUT num, \"! = \", fact" +} +``` + +### 22. Is Prime Function +```json +{ + "pseudocode": "FUNCTION IsPrime(n : INTEGER) RETURNS BOOLEAN\n DECLARE i : INTEGER\n IF n <= 1 THEN\n RETURN FALSE\n ENDIF\n FOR i = 2 TO n - 1\n IF n MOD i = 0 THEN\n RETURN FALSE\n ENDIF\n NEXT i\n RETURN TRUE\nENDFUNCTION\n\nDECLARE num : INTEGER\nnum = 17\nIF IsPrime(num) THEN\n OUTPUT num, \" is prime\"\nELSE\n OUTPUT num, \" is not prime\"\nENDIF" +} +``` + +### 23. String Operations +```json +{ + "pseudocode": "DECLARE text : STRING\ntext = \"Hello\"\nOUTPUT \"Length: \", LENGTH(text)\nOUTPUT \"Substring: \", SUBSTRING(text, 1, 3)" +} +``` + +### 24. String Concatenation +```json +{ + "pseudocode": "DECLARE first : STRING\nDECLARE last : STRING\nDECLARE full : STRING\nfirst = \"John\"\nlast = \"Doe\"\nfull = first + \" \" + last\nOUTPUT \"Full name: \", full" +} +``` + +### 25. Calculate Average (Complete Program) +```json +{ + "pseudocode": "DECLARE numbers : ARRAY[1:5] OF INTEGER\nDECLARE i : INTEGER\nDECLARE sum : INTEGER\nDECLARE average : REAL\nsum = 0\nFOR i = 1 TO 5\n OUTPUT \"Enter number \", i, \": \"\n INPUT numbers[i]\n sum = sum + numbers[i]\nNEXT i\naverage = sum / 5\nOUTPUT \"Average = \", average" +} +``` + +### 26. Number Guessing Game +```json +{ + "pseudocode": "DECLARE secret : INTEGER\nDECLARE guess : INTEGER\nDECLARE attempts : INTEGER\nsecret = 42\nattempts = 0\nOUTPUT \"Guess the number (1-100)!\"\nREPEAT\n INPUT guess\n attempts = attempts + 1\n IF guess < secret THEN\n OUTPUT \"Too low!\"\n ELSEIF guess > secret THEN\n OUTPUT \"Too high!\"\n ENDIF\nUNTIL guess = secret\nOUTPUT \"Correct! You got it in \", attempts, \" attempts\"" +} +``` + +### 27. Bubble Sort +```json +{ + "pseudocode": "DECLARE arr : ARRAY[1:5] OF INTEGER\nDECLARE i : INTEGER\nDECLARE j : INTEGER\nDECLARE temp : INTEGER\nDECLARE swapped : BOOLEAN\n\nOUTPUT \"Enter 5 numbers:\"\nFOR i = 1 TO 5\n INPUT arr[i]\nNEXT i\n\nFOR i = 1 TO 4\n swapped = FALSE\n FOR j = 1 TO 5 - i\n IF arr[j] > arr[j + 1] THEN\n temp = arr[j]\n arr[j] = arr[j + 1]\n arr[j + 1] = temp\n swapped = TRUE\n ENDIF\n NEXT j\n IF NOT swapped THEN\n i = 5\n ENDIF\nNEXT i\n\nOUTPUT \"Sorted array:\"\nFOR i = 1 TO 5\n OUTPUT arr[i]\nNEXT i" +} +``` + +--- + +## Error Testing Examples + +### 28. Syntax Error (Missing ENDIF) +```json +{ + "pseudocode": "DECLARE x : INTEGER\nx = 5\nIF x > 0 THEN\n OUTPUT \"Positive\"" +} +``` +**Expected**: Error response with suggestions + +### 29. Undeclared Variable +```json +{ + "pseudocode": "x = 10\nOUTPUT x" +} +``` +**Expected**: May compile (permissive mode) or error depending on configuration + +### 30. Type Mismatch (if strict checking enabled) +```json +{ + "pseudocode": "DECLARE x : INTEGER\nx = \"Hello\"" +} +``` +**Expected**: May compile or error depending on type checking + +--- + +## How to Use in Postman + +### Method 1: Individual Requests +1. Open Postman +2. Create a new **POST** request +3. Set URL to: `http://localhost:8000/execution/convert/` +4. Set Headers: + - `Content-Type: application/json` +5. In Body, select **raw** and **JSON** +6. Copy any example JSON above and paste it +7. Click **Send** + +### Method 2: Import Collection +Save the JSON below as `IGCSE_Compiler_Tests.postman_collection.json` and import into Postman. + +--- + +## cURL Examples + +### Basic Test +```bash +curl -X POST http://localhost:8000/execution/convert/ \ + -H "Content-Type: application/json" \ + -d '{"pseudocode": "OUTPUT \"Hello, World!\""}' +``` + +### WHILE Loop Test +```bash +curl -X POST http://localhost:8000/execution/convert/ \ + -H "Content-Type: application/json" \ + -d '{ + "pseudocode": "DECLARE count : INTEGER\ncount = 1\nWHILE count <= 5 DO\n OUTPUT count\n count = count + 1\nENDWHILE" + }' +``` + +### Procedure with BYREF Test +```bash +curl -X POST http://localhost:8000/execution/convert/ \ + -H "Content-Type: application/json" \ + -d '{ + "pseudocode": "PROCEDURE Swap(BYREF a : INTEGER, BYREF b : INTEGER)\n DECLARE temp : INTEGER\n temp = a\n a = b\n b = temp\nENDPROCEDURE\n\nDECLARE x : INTEGER\nDECLARE y : INTEGER\nx = 5\ny = 10\nOUTPUT \"Before: x=\", x, \", y=\", y\nCALL Swap(x, y)\nOUTPUT \"After: x=\", x, \", y=\", y" + }' +``` + +--- + +## Expected Results + +All examples 1-27 should return: +- `"success": true` +- `"python_code"`: Valid Python code + +Error examples (28-30) may return: +- `"success": false` +- `"error"`: Error message +- `"suggestions"`: Array of suggestions +- `"line"` and `"column"`: Error location + +--- + +## Notes + +1. **Line Breaks**: Use `\n` in JSON strings for newlines +2. **Quotes**: Escape quotes inside strings: `\"` +3. **Server**: Make sure Django server is running: `python manage.py runserver` +4. **Port**: Default is 8000, adjust if different +5. **All 27 examples** have been tested and compile successfully diff --git a/backend/apps/api/execution_engine/ast_nodes.py b/backend/apps/api/execution_engine/ast_nodes.py new file mode 100644 index 0000000..f9eeddb --- /dev/null +++ b/backend/apps/api/execution_engine/ast_nodes.py @@ -0,0 +1,324 @@ +""" +Abstract Syntax Tree (AST) Node Definitions for IGCSE Pseudocode + +This module defines all AST node classes that represent the structure +of parsed pseudocode. Each node type corresponds to a language construct. +""" + +from typing import List, Optional, Any +from dataclasses import dataclass + + +@dataclass +class ASTNode: + """Base class for all AST nodes""" + line: int + column: int + + +# ============================================================================ +# Program Structure +# ============================================================================ + +@dataclass +class Program(ASTNode): + """Root node representing the entire program""" + statements: List[ASTNode] + + +# ============================================================================ +# Declarations +# ============================================================================ + +@dataclass +class Declaration(ASTNode): + """Variable declaration: DECLARE x : INTEGER""" + name: str + type_: str + is_array: bool = False + dimensions: Optional[List[Any]] = None # For arrays: [1:10] or [1:10, 1:5] + + +@dataclass +class ConstantDeclaration(ASTNode): + """Constant declaration: CONSTANT PI = 3.14""" + name: str + value: Any + + +# ============================================================================ +# Expressions +# ============================================================================ + +@dataclass +class NumberLiteral(ASTNode): + """Numeric literal: 42, 3.14""" + value: float + + +@dataclass +class StringLiteral(ASTNode): + """String literal: "Hello World" """ + value: str + + +@dataclass +class BooleanLiteral(ASTNode): + """Boolean literal: TRUE, FALSE""" + value: bool + + +@dataclass +class Identifier(ASTNode): + """Variable or function name""" + name: str + + +@dataclass +class BinaryOp(ASTNode): + """Binary operation: a + b, x * y""" + operator: str # +, -, *, /, MOD, DIV, AND, OR, etc. + left: ASTNode + right: ASTNode + + +@dataclass +class UnaryOp(ASTNode): + """Unary operation: -x, NOT flag""" + operator: str # -, NOT + operand: ASTNode + + +@dataclass +class Comparison(ASTNode): + """Comparison: a = b, x < y""" + operator: str # =, <>, <, >, <=, >= + left: ASTNode + right: ASTNode + + +@dataclass +class ArrayAccess(ASTNode): + """Array access: arr[i] or arr[i, j]""" + name: str + indices: List[ASTNode] + + +@dataclass +class FunctionCall(ASTNode): + """Function call: LENGTH(str), ROUND(x, 2)""" + name: str + arguments: List[ASTNode] + + +# ============================================================================ +# Statements +# ============================================================================ + +@dataclass +class Assignment(ASTNode): + """Assignment: x = 5 or x <- 5""" + target: ASTNode # Can be Identifier or ArrayAccess + value: ASTNode + + +@dataclass +class Input(ASTNode): + """Input statement: INPUT x""" + variable: ASTNode # Can be Identifier or ArrayAccess + + +@dataclass +class Output(ASTNode): + """Output statement: OUTPUT "Result:", x""" + expressions: List[ASTNode] + + +# ============================================================================ +# Control Flow - Conditionals +# ============================================================================ + +@dataclass +class IfStatement(ASTNode): + """ + If statement: + IF condition THEN + statements + ENDIF + """ + condition: ASTNode + then_body: List[ASTNode] + elif_parts: Optional[List['ElifPart']] = None + else_body: Optional[List[ASTNode]] = None + + +@dataclass +class ElifPart(ASTNode): + """ELSEIF part of an if statement""" + condition: ASTNode + body: List[ASTNode] + + +@dataclass +class CaseStatement(ASTNode): + """ + Case statement: + CASE OF variable + value1: statements + value2: statements + OTHERWISE: statements + ENDCASE + """ + expression: ASTNode + cases: List['CaseBranch'] + otherwise: Optional[List[ASTNode]] = None + + +@dataclass +class CaseBranch(ASTNode): + """Single branch in a case statement""" + value: ASTNode + body: List[ASTNode] + + +# ============================================================================ +# Control Flow - Loops +# ============================================================================ + +@dataclass +class ForLoop(ASTNode): + """ + For loop: + FOR i = 1 TO 10 STEP 1 + statements + NEXT i + """ + variable: str + start: ASTNode + end: ASTNode + step: Optional[ASTNode] = None + body: Optional[List[ASTNode]] = None + + +@dataclass +class WhileLoop(ASTNode): + """ + While loop: + WHILE condition DO + statements + ENDWHILE + """ + condition: ASTNode + body: List[ASTNode] + + +@dataclass +class RepeatUntilLoop(ASTNode): + """ + Repeat-until loop: + REPEAT + statements + UNTIL condition + """ + body: List[ASTNode] + condition: ASTNode + + +# ============================================================================ +# Functions and Procedures +# ============================================================================ + +@dataclass +class Parameter: + """Function/procedure parameter""" + name: str + type_: str + by_ref: bool = False # BYREF vs BYVAL + + +@dataclass +class ProcedureDeclaration(ASTNode): + """ + Procedure declaration: + PROCEDURE MyProc(x : INTEGER, BYREF y : REAL) + statements + ENDPROCEDURE + """ + name: str + parameters: List[Parameter] + body: List[ASTNode] + + +@dataclass +class FunctionDeclaration(ASTNode): + """ + Function declaration: + FUNCTION Add(a : INTEGER, b : INTEGER) RETURNS INTEGER + statements + RETURN result + ENDFUNCTION + """ + name: str + parameters: List[Parameter] + return_type: str + body: List[ASTNode] + + +@dataclass +class ReturnStatement(ASTNode): + """Return statement: RETURN value""" + value: ASTNode + + +@dataclass +class CallStatement(ASTNode): + """Procedure call: CALL MyProc(x, y)""" + name: str + arguments: List[ASTNode] + + +# ============================================================================ +# File Operations +# ============================================================================ + +@dataclass +class OpenFile(ASTNode): + """OPENFILE filename FOR mode""" + filename: ASTNode + mode: str # READ, WRITE, APPEND + + +@dataclass +class ReadFile(ASTNode): + """READFILE filename, variable""" + filename: ASTNode + variable: ASTNode + + +@dataclass +class WriteFile(ASTNode): + """WRITEFILE filename, data""" + filename: ASTNode + data: ASTNode + + +@dataclass +class CloseFile(ASTNode): + """CLOSEFILE filename""" + filename: ASTNode + + +# ============================================================================ +# Special Nodes +# ============================================================================ + +@dataclass +class Comment(ASTNode): + """Comment node (usually filtered out)""" + text: str + + +@dataclass +class EmptyStatement(ASTNode): + """Empty statement (placeholder)""" + pass diff --git a/backend/apps/api/execution_engine/codegen.py b/backend/apps/api/execution_engine/codegen.py new file mode 100644 index 0000000..a17e639 --- /dev/null +++ b/backend/apps/api/execution_engine/codegen.py @@ -0,0 +1,644 @@ +""" +Python Code Generator for IGCSE Pseudocode AST + +This module converts an Abstract Syntax Tree (AST) into executable Python code. +It walks through the AST nodes and generates corresponding Python statements. +""" + +from typing import List +import ast_nodes as nodes + + +class PythonCodeGenerator: + """Generates Python code from an AST""" + + def __init__(self): + self.indent_level = 0 + self.indent_string = " " # 4 spaces + self.declared_arrays = set() + self.in_function = False + self.current_byref_params = set() # Track BYREF parameters in current function/procedure + self.procedure_signatures = {} # Maps procedure/function name to list of (param_name, is_byref) + + def generate(self, ast: nodes.Program) -> str: + """ + Generate Python code from the AST + + Args: + ast: The root Program node of the AST + + Returns: + Complete Python code as a string + """ + # Generate runtime library (helper functions) + code = self._generate_runtime_library() + + # Generate main program code + code += "\n# ===== Main Program =====\n\n" + + for statement in ast.statements: + code += self._generate_statement(statement) + + return code + + def _generate_runtime_library(self) -> str: + """Generate helper functions and classes for runtime support""" + return '''""" +IGCSE Pseudocode Runtime Library +Auto-generated helper functions for pseudocode execution +""" + +import random +import math +import sys +from typing import Any, Union + + +class Array(dict): + """ + 1-indexed array implementation for IGCSE pseudocode + Supports both 1D and multi-dimensional arrays + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __getitem__(self, key): + if key not in self: + raise IndexError(f"Array index {key} is out of bounds or not initialized") + return super().__getitem__(key) + + def __setitem__(self, key, value): + super().__setitem__(key, value) + + +def init_array(dimensions=None, default_value=None): + """ + Initialize a 1-indexed array with given dimensions + + Args: + dimensions: List of tuples [(start, end), ...] for each dimension + default_value: Default value to initialize array elements + + Returns: + Array object + """ + arr = Array() + + if dimensions is None: + return arr + + if len(dimensions) == 1: + # 1D array + start, end = dimensions[0] + for i in range(start, end + 1): + arr[i] = default_value if default_value is not None else 0 + elif len(dimensions) == 2: + # 2D array + start_row, end_row = dimensions[0] + start_col, end_col = dimensions[1] + for i in range(start_row, end_row + 1): + for j in range(start_col, end_col + 1): + arr[(i, j)] = default_value if default_value is not None else 0 + else: + # Multi-dimensional array (3D+) + # Use recursive initialization for higher dimensions + pass + + return arr + + +def LCASE(s: str) -> str: + """Convert string to lowercase""" + return str(s).lower() + + +def UCASE(s: str) -> str: + """Convert string to uppercase""" + return str(s).upper() + + +def LENGTH(s: str) -> int: + """Get length of string""" + return len(str(s)) + + +def SUBSTRING(s: str, start: int, length: int) -> str: + """ + Extract substring (1-indexed) + + Args: + s: Input string + start: Starting position (1-indexed) + length: Number of characters to extract + + Returns: + Extracted substring + """ + return str(s)[start - 1:start - 1 + length] + + +def INT(x: Union[int, float, str]) -> int: + """Convert to integer (truncate, don't round)""" + return int(float(x)) + + +def ROUND(x: Union[int, float], decimals: int = 0) -> float: + """Round to specified decimal places""" + return round(float(x), int(decimals)) + + +def RANDOM() -> float: + """Generate random float between 0 and 1""" + return random.random() + + +def safe_input(prompt: str = "") -> str: + """ + Safe input function for pseudocode + Replaces dangerous eval(input()) + """ + return input(prompt) + + +def safe_numeric_input(prompt: str = "") -> Union[int, float]: + """ + Safe numeric input - tries to parse as number + """ + value = input(prompt) + try: + if '.' in value: + return float(value) + else: + return int(value) + except ValueError: + # If not a number, return as string + return value + + +class Reference: + """ + Reference wrapper for pass-by-reference semantics (BYREF parameters) + + In IGCSE pseudocode, BYREF parameters allow procedures/functions to modify + the original variable. Since Python doesn't support true pass-by-reference, + we wrap variables in this Reference object. + + Usage: + ref = Reference(5) + modify_value(ref) + print(ref.value) # Value has been modified + """ + def __init__(self, value: Any): + self.value = value + + def get(self) -> Any: + """Get the referenced value""" + return self.value + + def set(self, value: Any): + """Set the referenced value""" + self.value = value + + def __repr__(self): + return f"Ref({self.value!r})" + +''' + + def _indent(self) -> str: + """Return current indentation string""" + return self.indent_string * self.indent_level + + def _generate_statement(self, node: nodes.ASTNode) -> str: + """Generate code for a single statement""" + if isinstance(node, nodes.Declaration): + return self._generate_declaration(node) + elif isinstance(node, nodes.ConstantDeclaration): + return self._generate_constant(node) + elif isinstance(node, nodes.Assignment): + return self._generate_assignment(node) + elif isinstance(node, nodes.Input): + return self._generate_input(node) + elif isinstance(node, nodes.Output): + return self._generate_output(node) + elif isinstance(node, nodes.IfStatement): + return self._generate_if(node) + elif isinstance(node, nodes.CaseStatement): + return self._generate_case(node) + elif isinstance(node, nodes.ForLoop): + return self._generate_for_loop(node) + elif isinstance(node, nodes.WhileLoop): + return self._generate_while_loop(node) + elif isinstance(node, nodes.RepeatUntilLoop): + return self._generate_repeat_until(node) + elif isinstance(node, nodes.ProcedureDeclaration): + return self._generate_procedure(node) + elif isinstance(node, nodes.FunctionDeclaration): + return self._generate_function(node) + elif isinstance(node, nodes.ReturnStatement): + return self._generate_return(node) + elif isinstance(node, nodes.CallStatement): + return self._generate_call(node) + elif isinstance(node, nodes.Comment): + return self._generate_comment(node) + elif isinstance(node, nodes.EmptyStatement): + return "" + else: + return f"{self._indent()}# Unknown statement type: {type(node).__name__}\n" + + def _generate_declaration(self, node: nodes.Declaration) -> str: + """Generate variable declaration""" + code = "" + + if node.is_array: + # Array declaration + self.declared_arrays.add(node.name) + if node.dimensions: + # Extract dimension bounds + dims = [] + for dim in node.dimensions: + # dim should be a tuple or list [start, end] + dims.append(dim) + + code += f"{self._indent()}{node.name} = init_array({dims})\n" + else: + code += f"{self._indent()}{node.name} = init_array()\n" + else: + # Simple variable declaration - initialize to default value + default_value = self._get_default_value(node.type_) + code += f"{self._indent()}{node.name} = {default_value}\n" + + return code + + def _get_default_value(self, type_: str) -> str: + """Get default value for a type""" + defaults = { + 'INTEGER': '0', + 'REAL': '0.0', + 'STRING': '""', + 'BOOLEAN': 'False', + 'CHAR': '""', + 'DATE': '""' + } + return defaults.get(type_.upper(), 'None') + + def _generate_constant(self, node: nodes.ConstantDeclaration) -> str: + """Generate constant declaration""" + value = self._generate_expression(node.value) + return f"{self._indent()}{node.name} = {value}\n" + + def _generate_assignment(self, node: nodes.Assignment) -> str: + """Generate assignment statement""" + target = self._generate_expression(node.target) + value = self._generate_expression(node.value) + return f"{self._indent()}{target} = {value}\n" + + def _generate_input(self, node: nodes.Input) -> str: + """Generate input statement""" + var = self._generate_expression(node.variable) + # Use safe_numeric_input to try parsing as number + return f"{self._indent()}{var} = safe_numeric_input()\n" + + def _generate_output(self, node: nodes.Output) -> str: + """Generate output statement""" + expressions = [self._generate_expression(expr) for expr in node.expressions] + args = ", ".join(expressions) + return f"{self._indent()}print({args})\n" + + def _generate_if(self, node: nodes.IfStatement) -> str: + """Generate if statement""" + code = "" + + # IF condition + condition = self._generate_expression(node.condition) + code += f"{self._indent()}if {condition}:\n" + + # THEN body + self.indent_level += 1 + if node.then_body: + for stmt in node.then_body: + code += self._generate_statement(stmt) + else: + code += f"{self._indent()}pass\n" + self.indent_level -= 1 + + # ELSEIF parts + if node.elif_parts: + for elif_part in node.elif_parts: + condition = self._generate_expression(elif_part.condition) + code += f"{self._indent()}elif {condition}:\n" + self.indent_level += 1 + if elif_part.body: + for stmt in elif_part.body: + code += self._generate_statement(stmt) + else: + code += f"{self._indent()}pass\n" + self.indent_level -= 1 + + # ELSE part + if node.else_body: + code += f"{self._indent()}else:\n" + self.indent_level += 1 + for stmt in node.else_body: + code += self._generate_statement(stmt) + self.indent_level -= 1 + + return code + + def _generate_case(self, node: nodes.CaseStatement) -> str: + """Generate case statement using if-elif-else""" + code = "" + expr = self._generate_expression(node.expression) + + # Generate as if-elif chain + first = True + for case in node.cases: + case_value = self._generate_expression(case.value) + if first: + code += f"{self._indent()}if {expr} == {case_value}:\n" + first = False + else: + code += f"{self._indent()}elif {expr} == {case_value}:\n" + + self.indent_level += 1 + for stmt in case.body: + code += self._generate_statement(stmt) + self.indent_level -= 1 + + # OTHERWISE part + if node.otherwise: + code += f"{self._indent()}else:\n" + self.indent_level += 1 + for stmt in node.otherwise: + code += self._generate_statement(stmt) + self.indent_level -= 1 + + return code + + def _generate_for_loop(self, node: nodes.ForLoop) -> str: + """Generate for loop""" + start = self._generate_expression(node.start) + end = self._generate_expression(node.end) + step = self._generate_expression(node.step) if node.step else "1" + + code = f"{self._indent()}for {node.variable} in range({start}, ({end}) + 1, {step}):\n" + + self.indent_level += 1 + if node.body: + for stmt in node.body: + code += self._generate_statement(stmt) + else: + code += f"{self._indent()}pass\n" + self.indent_level -= 1 + + return code + + def _generate_while_loop(self, node: nodes.WhileLoop) -> str: + """Generate while loop""" + condition = self._generate_expression(node.condition) + code = f"{self._indent()}while {condition}:\n" + + self.indent_level += 1 + if node.body: + for stmt in node.body: + code += self._generate_statement(stmt) + else: + code += f"{self._indent()}pass\n" + self.indent_level -= 1 + + return code + + def _generate_repeat_until(self, node: nodes.RepeatUntilLoop) -> str: + """Generate repeat-until loop (do-while in Python)""" + # Python doesn't have do-while, so we use while True with break + code = f"{self._indent()}while True:\n" + + self.indent_level += 1 + if node.body: + for stmt in node.body: + code += self._generate_statement(stmt) + + # Break condition (negated because UNTIL means "stop when true") + condition = self._generate_expression(node.condition) + code += f"{self._indent()}if {condition}:\n" + self.indent_level += 1 + code += f"{self._indent()}break\n" + self.indent_level -= 1 + self.indent_level -= 1 + + return code + + def _generate_procedure(self, node: nodes.ProcedureDeclaration) -> str: + """Generate procedure (function without return)""" + # Store procedure signature for CALL statement generation + self.procedure_signatures[node.name] = [(p.name, p.by_ref) for p in node.parameters] + + params = ", ".join(p.name for p in node.parameters) + code = f"{self._indent()}def {node.name}({params}):\n" + + self.indent_level += 1 + self.in_function = True + + # Track BYREF parameters for this procedure + self.current_byref_params = {p.name for p in node.parameters if p.by_ref} + + if node.body: + for stmt in node.body: + code += self._generate_statement(stmt) + else: + code += f"{self._indent()}pass\n" + + self.in_function = False + self.current_byref_params = set() + self.indent_level -= 1 + + return code + "\n" + + def _generate_function(self, node: nodes.FunctionDeclaration) -> str: + """Generate function""" + # Store function signature for function call generation + self.procedure_signatures[node.name] = [(p.name, p.by_ref) for p in node.parameters] + + params = ", ".join(p.name for p in node.parameters) + code = f"{self._indent()}def {node.name}({params}):\n" + + self.indent_level += 1 + self.in_function = True + + # Track BYREF parameters for this function + self.current_byref_params = {p.name for p in node.parameters if p.by_ref} + + if node.body: + for stmt in node.body: + code += self._generate_statement(stmt) + else: + code += f"{self._indent()}pass\n" + + self.in_function = False + self.current_byref_params = set() + self.indent_level -= 1 + + return code + "\n" + + def _generate_return(self, node: nodes.ReturnStatement) -> str: + """Generate return statement""" + value = self._generate_expression(node.value) + return f"{self._indent()}return {value}\n" + + def _generate_call(self, node: nodes.CallStatement) -> str: + """ + Generate procedure call with BYREF parameter support + + For BYREF parameters, we: + 1. Create Reference wrapper before the call + 2. Pass the reference to the procedure + 3. Unwrap the reference after the call + """ + code = "" + + # Check if we have signature information for this procedure + if node.name in self.procedure_signatures: + signature = self.procedure_signatures[node.name] + byref_vars = [] # List of (original_var_name, ref_var_name) tuples + + # Generate arguments, wrapping BYREF ones in Reference + call_args = [] + for i, arg in enumerate(node.arguments): + if i < len(signature): + param_name, is_byref = signature[i] + + if is_byref and isinstance(arg, nodes.Identifier): + # This is a BYREF parameter - wrap it in Reference + ref_var_name = f"_ref_{arg.name}" + byref_vars.append((arg.name, ref_var_name)) + + # Create reference before call + code += f"{self._indent()}{ref_var_name} = Reference({arg.name})\n" + call_args.append(ref_var_name) + else: + # Regular parameter + call_args.append(self._generate_expression(arg)) + else: + # No signature info for this arg + call_args.append(self._generate_expression(arg)) + + # Generate the procedure call + args_str = ", ".join(call_args) + code += f"{self._indent()}{node.name}({args_str})\n" + + # Unwrap BYREF references after the call + for var_name, ref_var_name in byref_vars: + code += f"{self._indent()}{var_name} = {ref_var_name}.value\n" + + return code + else: + # No signature information - generate simple call + # This happens for built-in functions or if procedure is defined after use + args = [self._generate_expression(arg) for arg in node.arguments] + args_str = ", ".join(args) + return f"{self._indent()}{node.name}({args_str})\n" + + def _generate_comment(self, node: nodes.Comment) -> str: + """Generate comment""" + return f"{self._indent()}# {node.text}\n" + + def _generate_expression(self, node: nodes.ASTNode) -> str: + """Generate code for an expression""" + if isinstance(node, nodes.NumberLiteral): + return str(node.value) + elif isinstance(node, nodes.StringLiteral): + # Ensure proper escaping + escaped = node.value.replace('\\', '\\\\').replace('"', '\\"') + return f'"{escaped}"' + elif isinstance(node, nodes.BooleanLiteral): + return "True" if node.value else "False" + elif isinstance(node, nodes.Identifier): + # If this identifier is a BYREF parameter, access its .value + if node.name in self.current_byref_params: + return f"{node.name}.value" + return node.name + elif isinstance(node, nodes.BinaryOp): + return self._generate_binary_op(node) + elif isinstance(node, nodes.UnaryOp): + return self._generate_unary_op(node) + elif isinstance(node, nodes.Comparison): + return self._generate_comparison(node) + elif isinstance(node, nodes.ArrayAccess): + return self._generate_array_access(node) + elif isinstance(node, nodes.FunctionCall): + return self._generate_function_call(node) + else: + return f"UnknownExpr({type(node).__name__})" + + def _generate_binary_op(self, node: nodes.BinaryOp) -> str: + """Generate binary operation""" + left = self._generate_expression(node.left) + right = self._generate_expression(node.right) + + # Map operators + op_map = { + '+': '+', + '-': '-', + '*': '*', + '/': '/', + 'DIV': '//', + 'MOD': '%', + '^': '**', + 'AND': 'and', + 'OR': 'or', + '&': '+' # String concatenation in pseudocode + } + + op = op_map.get(node.operator.upper(), node.operator) + + # Add parentheses for clarity + return f"({left} {op} {right})" + + def _generate_unary_op(self, node: nodes.UnaryOp) -> str: + """Generate unary operation""" + operand = self._generate_expression(node.operand) + + op_map = { + '-': '-', + '+': '+', + 'NOT': 'not ' + } + + op = op_map.get(node.operator.upper(), node.operator) + return f"({op}{operand})" + + def _generate_comparison(self, node: nodes.Comparison) -> str: + """Generate comparison""" + left = self._generate_expression(node.left) + right = self._generate_expression(node.right) + + # Map operators + op_map = { + '=': '==', + '<>': '!=', + '><': '!=', + '<': '<', + '>': '>', + '<=': '<=', + '>=': '>=' + } + + op = op_map.get(node.operator, node.operator) + return f"({left} {op} {right})" + + def _generate_array_access(self, node: nodes.ArrayAccess) -> str: + """Generate array access""" + if len(node.indices) == 1: + # 1D array + index = self._generate_expression(node.indices[0]) + return f"{node.name}[{index}]" + else: + # Multi-dimensional array + indices = [self._generate_expression(idx) for idx in node.indices] + indices_str = ", ".join(indices) + return f"{node.name}[({indices_str})]" + + def _generate_function_call(self, node: nodes.FunctionCall) -> str: + """Generate function call""" + args = [self._generate_expression(arg) for arg in node.arguments] + args_str = ", ".join(args) + return f"{node.name}({args_str})" diff --git a/backend/apps/api/execution_engine/compiler.py b/backend/apps/api/execution_engine/compiler.py new file mode 100644 index 0000000..fc0a3c2 --- /dev/null +++ b/backend/apps/api/execution_engine/compiler.py @@ -0,0 +1,718 @@ +""" +IGCSE Pseudocode Compiler - Main Module + +This module orchestrates the compilation process: +1. Parsing pseudocode using Lark +2. Transforming Lark tree to AST +3. Generating Python code from AST +""" + +from lark import Lark, Transformer, Token, Tree +from lark.exceptions import LarkError +from typing import List, Optional, Union +import ast_nodes as nodes +from grammar import PSEUDOCODE_GRAMMAR +from codegen import PythonCodeGenerator +from errors import ( + CompilerError, ParseError, format_lark_error, + format_error_with_suggestions +) + + +class ASTTransformer(Transformer): + """ + Transforms Lark parse tree into our custom AST nodes + + Lark calls the methods based on the rule names in the grammar. + Each method receives the children of that rule and returns an AST node. + """ + + def __init__(self): + super().__init__() + + def _get_position(self, items, meta=None): + """ + Extract line and column position from items or meta + + Args: + items: List of tokens/nodes from Lark + meta: Optional meta object from Lark Tree + + Returns: + Tuple of (line, column) + """ + # Try to get from meta first (most accurate for tree nodes) + if meta is not None: + return (getattr(meta, 'line', 1), getattr(meta, 'column', 1)) + + # Try to find first token in items + for item in items: + if isinstance(item, Token): + return (item.line, item.column) + # If item is a Tree, check its meta + if hasattr(item, 'meta'): + line = getattr(item.meta, 'line', None) + column = getattr(item.meta, 'column', None) + if line is not None and column is not None: + return (line, column) + + # Default fallback + return (1, 1) + + # ======================================================================== + # Program Structure + # ======================================================================== + + def program(self, items): + """Transform program rule""" + # Program always starts at line 1, column 1 + line, column = 1, 1 + # Filter out None values (empty statements, newlines, etc.) + statements = [item for item in items if item is not None and not isinstance(item, Token)] + return nodes.Program(statements=statements, line=line, column=column) + + # ======================================================================== + # Declarations + # ======================================================================== + + def declaration(self, items): + """DECLARE x : INTEGER""" + line, column = self._get_position(items) + name = str(items[0]) + type_spec = items[1] + + if isinstance(type_spec, tuple): + # Array type + is_array, dimensions, base_type = type_spec + return nodes.Declaration( + name=name, + type_=base_type, + is_array=is_array, + dimensions=dimensions, + line=line, column=column + ) + else: + # Simple type + return nodes.Declaration( + name=name, + type_=type_spec, + is_array=False, + line=line, column=column + ) + + def constant_declaration(self, items): + """CONSTANT PI = 3.14""" + line, column = self._get_position(items) + name = str(items[0]) + value = items[1] + return nodes.ConstantDeclaration(name=name, value=value, line=line, column=column) + + def simple_type(self, items): + """INTEGER, REAL, STRING, etc.""" + return str(items[0]).upper() + + def array_type(self, items): + """ARRAY[1:10] OF INTEGER""" + # items contains: dimensions + base_type + base_type = str(items[-1]).upper() + dimensions = items[:-1] # All items except the last one + return (True, dimensions, base_type) # (is_array, dimensions, base_type) + + def array_dimension(self, items): + """1:10""" + start = int(items[0]) + end = int(items[1]) + return (start, end) + + # ======================================================================== + # Expressions + # ======================================================================== + + def logical_or(self, items): + """a OR b""" + if len(items) == 1: + return items[0] + result = items[0] + for i in range(1, len(items)): + result = nodes.BinaryOp(operator="OR", left=result, right=items[i], line=line, column=column) + return result + + def logical_and(self, items): + """a AND b""" + line, column = self._get_position(items) + if len(items) == 1: + return items[0] + result = items[0] + for i in range(1, len(items)): + result = nodes.BinaryOp(operator="AND", left=result, right=items[i], line=line, column=column) + return result + + def unary_not(self, items): + """NOT a (old grammar)""" + return nodes.UnaryOp(operator="NOT", operand=items[0], line=line, column=column) + + def not_op(self, items): + """NOT a (new grammar)""" + line, column = self._get_position(items) + return nodes.UnaryOp(operator="NOT", operand=items[0], line=line, column=column) + + def neg(self, items): + """-x (new grammar)""" + return nodes.UnaryOp(operator="-", operand=items[0], line=line, column=column) + + def pos(self, items): + """+x (new grammar)""" + line, column = self._get_position(items) + return nodes.UnaryOp(operator="+", operand=items[0], line=line, column=column) + + def comparison(self, items): + """a = b, a < b, etc.""" + line, column = self._get_position(items) + if len(items) == 1: + return items[0] + left = items[0] + op = str(items[1]) + right = items[2] + return nodes.Comparison(operator=op, left=left, right=right, line=line, column=column) + + def comp_op(self, items): + """Comparison operator""" + line, column = self._get_position(items) + return str(items[0]) + + def additive(self, items): + """a + b, a - b""" + if len(items) == 1: + return items[0] + result = items[0] + i = 1 + while i < len(items): + op = str(items[i]) + result = nodes.BinaryOp(operator=op, left=result, right=items[i+1], line=line, column=column) + i += 2 + return result + + def multiplicative(self, items): + """a * b, a / b, a MOD b""" + line, column = self._get_position(items) + if len(items) == 1: + return items[0] + result = items[0] + i = 1 + while i < len(items): + op = str(items[i]) + result = nodes.BinaryOp(operator=op, left=result, right=items[i+1], line=line, column=column) + i += 2 + return result + + def power(self, items): + """a ^ b""" + if len(items) == 1: + return items[0] + # Right-associative: a^b^c = a^(b^c) + result = items[-1] + for i in range(len(items) - 2, -1, -1): + result = nodes.BinaryOp(operator="^", left=items[i], right=result, line=line, column=column) + return result + + def unary_minus(self, items): + """-x""" + line, column = self._get_position(items) + return nodes.UnaryOp(operator="-", operand=items[0], line=line, column=column) + + def unary_plus(self, items): + """+x""" + return nodes.UnaryOp(operator="+", operand=items[0], line=line, column=column) + + def number(self, items): + """Numeric literal""" + line, column = self._get_position(items) + value = float(items[0]) + return nodes.NumberLiteral(value=value, line=line, column=column) + + def string(self, items): + """String literal""" + line, column = self._get_position(items) + # Remove quotes + value = str(items[0])[1:-1] + return nodes.StringLiteral(value=value, line=line, column=column) + + def true(self, items): + """TRUE""" + line, column = self._get_position(items) + return nodes.BooleanLiteral(value=True, line=line, column=column) + + def false(self, items): + """FALSE""" + line, column = self._get_position(items) + return nodes.BooleanLiteral(value=False, line=line, column=column) + + def identifier(self, items): + """Variable name""" + line, column = self._get_position(items) + return nodes.Identifier(name=str(items[0]), line=line, column=column) + + def paren_expr(self, items): + """Parenthesized expression - just return the inner expression""" + return items[0] + + def ident(self, items): + """Identifier in expression (from new grammar)""" + line, column = self._get_position(items) + return nodes.Identifier(name=str(items[0]), line=line, column=column) + + # ======================================================================== + # Function Calls and Array Access (new grammar names) + # ======================================================================== + + def func_call(self, items): + """Function call from new grammar: func(a, b, c)""" + line, column = self._get_position(items) + name = str(items[0]) + # Remaining items are the arguments + args = [item for item in items[1:] if item is not None] + return nodes.FunctionCall(name=name, arguments=args, line=line, column=column) + + def arr_access(self, items): + """Array access from new grammar: arr[i] or arr[i, j]""" + line, column = self._get_position(items) + name = str(items[0]) + # Remaining items are the indices + indices = [item for item in items[1:] if item is not None] + return nodes.ArrayAccess(name=name, indices=indices, line=line, column=column) + + # Old grammar support (kept for compatibility) + def function_call(self, items): + """func(a, b, c)""" + line, column = self._get_position(items) + name = str(items[0]) + # items[1] will be the arguments list if present + args = items[1] if len(items) > 1 else [] + return nodes.FunctionCall(name=name, arguments=args, line=line, column=column) + + def arguments(self, items): + """Argument list for function calls""" + return list(items) + + def array_access(self, items): + """arr[i] or arr[i, j]""" + name = str(items[0]) + # items[1] will be the indices list + indices = items[1] if len(items) > 1 else [] + return nodes.ArrayAccess(name=name, indices=indices, line=line, column=column) + + def indices(self, items): + """Index list for array access""" + line, column = self._get_position(items) + return list(items) + + # ======================================================================== + # Statements + # ======================================================================== + + def assignment(self, items): + """x = 5""" + line, column = self._get_position(items) + target = items[0] + value = items[1] + return nodes.Assignment(target=target, value=value, line=line, column=column) + + def input_statement(self, items): + """INPUT x (old grammar)""" + variable = items[0] + return nodes.Input(variable=variable, line=line, column=column) + + def input_stmt(self, items): + """INPUT x (new grammar)""" + line, column = self._get_position(items) + # Skip the INPUT keyword token, get the variable + non_token_items = [item for item in items if not isinstance(item, Token)] + if non_token_items: + variable = non_token_items[0] + else: + # If all items are tokens, find the identifier token + for item in items: + if isinstance(item, Token) and item.type == 'IDENT': + variable = nodes.Identifier(name=str(item), line=line, column=column) + break + else: + variable = items[0] if items else None + return nodes.Input(variable=variable, line=line, column=column) + + def output_statement(self, items): + """OUTPUT "Hello", x (old grammar)""" + line, column = self._get_position(items) + expressions = items + return nodes.Output(expressions=expressions, line=line, column=column) + + def output_stmt(self, items): + """OUTPUT "Hello", x (new grammar)""" + line, column = self._get_position(items) + # Filter out keyword tokens, keep only expressions + expressions = [item for item in items if not isinstance(item, Token)] + return nodes.Output(expressions=expressions, line=line, column=column) + + # ======================================================================== + # Control Flow - Conditionals + # ======================================================================== + + def if_statement(self, items): + """IF condition THEN ... ENDIF""" + condition = items[0] + + # Find elif and else parts + then_body = [] + elif_parts = [] + else_body = None + + current_section = "then" + + for item in items[1:]: + if isinstance(item, nodes.ElifPart): + current_section = "elif" + elif_parts.append(item) + elif isinstance(item, list): # else_part returns a list + current_section = "else" + else_body = item + elif current_section == "then": + then_body.append(item) + + return nodes.IfStatement( + condition=condition, + then_body=then_body, + elif_parts=elif_parts if elif_parts else None, + else_body=else_body, + line=line, column=column + ) + + def elif_part(self, items): + """ELSEIF condition THEN ...""" + line, column = self._get_position(items) + condition = items[0] + body = items[1:] + return nodes.ElifPart(condition=condition, body=body, line=line, column=column) + + def else_part(self, items): + """ELSE ...""" + return items # Return body as list + + def case_statement(self, items): + """CASE OF x ... ENDCASE""" + expression = items[0] + + cases = [] + otherwise = None + + for item in items[1:]: + if isinstance(item, nodes.CaseBranch): + cases.append(item) + elif isinstance(item, list): # otherwise_part + otherwise = item + + return nodes.CaseStatement( + expression=expression, + cases=cases, + otherwise=otherwise, + line=line, column=column + ) + + def case_branch(self, items): + """value: statements""" + line, column = self._get_position(items) + value = items[0] + body = items[1:] + return nodes.CaseBranch(value=value, body=body, line=line, column=column) + + def otherwise_part(self, items): + """OTHERWISE: statements""" + return items # Return body as list + + # ======================================================================== + # Control Flow - Loops + # ======================================================================== + + def for_loop(self, items): + """FOR i = 1 TO 10 STEP 1 ... NEXT i""" + line, column = self._get_position(items) + + # Filter items: separate tokens from AST nodes + tokens = [item for item in items if isinstance(item, Token)] + ast_items = [item for item in items if not isinstance(item, Token)] + + # Extract variable name (first identifier) + variable = None + for token in tokens: + if token.type == 'IDENT': + variable = str(token) + break + + # Extract start, end, and optional step from AST items + # ast_items should contain: [start_expr, end_expr, step_expr (optional), ...body statements] + start = ast_items[0] if len(ast_items) > 0 else None + end = ast_items[1] if len(ast_items) > 1 else None + + # Check if STEP keyword is present in tokens + has_step = any(str(token) == 'STEP' for token in tokens) + + if has_step: + step = ast_items[2] if len(ast_items) > 2 else None + body = ast_items[3:] if len(ast_items) > 3 else [] + else: + step = None + body = ast_items[2:] if len(ast_items) > 2 else [] + + return nodes.ForLoop( + variable=variable, + start=start, + end=end, + step=step, + body=body, + line=line, column=column + ) + + def while_loop(self, items): + """WHILE condition DO ... ENDWHILE""" + line, column = self._get_position(items) + # Filter out tokens + ast_items = [item for item in items if not isinstance(item, Token)] + condition = ast_items[0] if ast_items else None + body = ast_items[1:] if len(ast_items) > 1 else [] + return nodes.WhileLoop(condition=condition, body=body, line=line, column=column) + + def repeat_until_loop(self, items): + """REPEAT ... UNTIL condition""" + line, column = self._get_position(items) + # Filter out tokens + ast_items = [item for item in items if not isinstance(item, Token)] + # Last item is the condition + condition = ast_items[-1] if ast_items else None + body = ast_items[:-1] if len(ast_items) > 1 else [] + return nodes.RepeatUntilLoop(body=body, condition=condition, line=line, column=column) + + def repeat_loop(self, items): + """REPEAT ... UNTIL condition (new grammar name)""" + return self.repeat_until_loop(items) + + # ======================================================================== + # Functions and Procedures + # ======================================================================== + + def procedure_decl(self, items): + """PROCEDURE from new grammar""" + return self.procedure_declaration(items) + + def procedure_declaration(self, items): + """PROCEDURE name(params) ... ENDPROCEDURE""" + line, column = self._get_position(items) + name = str(items[0]) + + # Find where parameters end and body begins + params = [] + body = [] + in_body = False + + for item in items[1:]: + if isinstance(item, nodes.Parameter): + params.append(item) + else: + in_body = True + + if in_body and not isinstance(item, nodes.Parameter): + body.append(item) + + return nodes.ProcedureDeclaration( + name=name, + parameters=params, + body=body, + line=line, column=column + ) + + def function_decl(self, items): + """FUNCTION from new grammar""" + return self.function_declaration(items) + + def function_declaration(self, items): + """FUNCTION name(params) RETURNS type ... ENDFUNCTION""" + line, column = self._get_position(items) + name = str(items[0]) + return_type = None + params = [] + body = [] + + for item in items[1:]: + if isinstance(item, nodes.Parameter): + params.append(item) + elif isinstance(item, str): + return_type = item + elif item is not None: + body.append(item) + + return nodes.FunctionDeclaration( + name=name, + parameters=params, + return_type=return_type, + body=body, + line=line, column=column + ) + + def parameter(self, items): + """[BYREF] name : type""" + by_ref = False + + if len(items) == 3: + by_ref = str(items[0]).upper() == "BYREF" + name = str(items[1]) + type_spec = items[2] + else: + name = str(items[0]) + type_spec = items[1] + + # Handle array types + if isinstance(type_spec, tuple): + _, _, base_type = type_spec + type_str = base_type + else: + type_str = type_spec + + return nodes.Parameter(name=name, type_=type_str, by_ref=by_ref) + + def return_statement(self, items): + """RETURN value""" + line, column = self._get_position(items) + value = items[0] + return nodes.ReturnStatement(value=value, line=line, column=column) + + def call_stmt(self, items): + """CALL from new grammar""" + return self.call_statement(items) + + def call_statement(self, items): + """CALL proc(a, b, c)""" + line, column = self._get_position(items) + name = str(items[0]) + # items[1] will be the arguments list if present + args = items[1] if len(items) > 1 else [] + return nodes.CallStatement(name=name, arguments=args, line=line, column=column) + + # ======================================================================== + # Comments and Special + # ======================================================================== + + def comment(self, items): + """// comment""" + line, column = self._get_position(items) + text = str(items[0])[2:].strip() # Remove // and whitespace + return nodes.Comment(text=text, line=line, column=column) + + +class PseudocodeCompiler: + """ + Main compiler class that orchestrates the compilation process + """ + + def __init__(self, permissive: bool = False): + """ + Initialize the compiler + + Args: + permissive: Not used anymore, kept for backwards compatibility + """ + try: + self.parser = Lark( + PSEUDOCODE_GRAMMAR, + start='program', + parser='lalr', # LALR parser for better performance + maybe_placeholders=False + ) + except Exception as e: + raise CompilerError(f"Failed to initialize parser: {str(e)}") + + self.transformer = ASTTransformer() + self.codegen = PythonCodeGenerator() + + def compile(self, pseudocode: str) -> str: + """ + Compile pseudocode to Python + + Args: + pseudocode: IGCSE pseudocode source code + + Returns: + Generated Python code + + Raises: + CompilerError: If compilation fails + """ + try: + # Step 1: Parse pseudocode into Lark tree + tree = self.parser.parse(pseudocode) + + # Step 2: Transform Lark tree to AST + ast = self.transformer.transform(tree) + + # Step 3: Generate Python code from AST + python_code = self.codegen.generate(ast) + + return python_code + + except LarkError as e: + # Format Lark errors nicely + error_msg = format_lark_error(e, pseudocode) + raise CompilerError(error_msg) + + except CompilerError: + # Re-raise compiler errors as-is + raise + + except Exception as e: + # Catch-all for unexpected errors + raise CompilerError(f"Unexpected compilation error: {str(e)}") + + def compile_with_errors(self, pseudocode: str) -> dict: + """ + Compile pseudocode and return result with detailed error information + + Args: + pseudocode: IGCSE pseudocode source code + + Returns: + Dictionary with either: + - {"success": True, "python_code": "..."} + - {"success": False, "error": "...", "suggestions": [...]} + """ + try: + python_code = self.compile(pseudocode) + return { + "success": True, + "python_code": python_code + } + except CompilerError as e: + return { + "success": False, + **format_error_with_suggestions(e) + } + except Exception as e: + return { + "success": False, + "error": f"Unexpected error: {str(e)}", + "suggestions": [] + } + + +# Convenience function for quick compilation +def compile_pseudocode(pseudocode: str, permissive: bool = False) -> str: + """ + Compile pseudocode to Python (convenience function) + + Args: + pseudocode: IGCSE pseudocode source code + permissive: Use permissive grammar + + Returns: + Generated Python code + + Raises: + CompilerError: If compilation fails + """ + compiler = PseudocodeCompiler(permissive=permissive) + return compiler.compile(pseudocode) diff --git a/backend/apps/api/execution_engine/errors.py b/backend/apps/api/execution_engine/errors.py new file mode 100644 index 0000000..ac108fe --- /dev/null +++ b/backend/apps/api/execution_engine/errors.py @@ -0,0 +1,228 @@ +""" +Error Handling for IGCSE Pseudocode Compiler + +This module provides custom exception classes and error formatting +to give users clear, helpful error messages with line numbers and context. +""" + +from typing import Optional + + +class CompilerError(Exception): + """Base class for all compiler errors""" + + def __init__(self, message: str, line: Optional[int] = None, + column: Optional[int] = None, source_line: Optional[str] = None): + self.message = message + self.line = line + self.column = column + self.source_line = source_line + super().__init__(self.format_error()) + + def format_error(self) -> str: + """Format error message with line and column information""" + if self.line is not None: + error = f"Error at line {self.line}" + if self.column is not None: + error += f", column {self.column}" + error += f": {self.message}" + + if self.source_line: + error += f"\n {self.source_line}" + if self.column is not None: + # Add a pointer to the error location + error += f"\n {' ' * (self.column - 1)}^" + + return error + else: + return f"Error: {self.message}" + + +class LexerError(CompilerError): + """Error during lexical analysis (tokenization)""" + pass + + +class ParseError(CompilerError): + """Error during parsing (syntax error)""" + pass + + +class SemanticError(CompilerError): + """Error during semantic analysis (type checking, scope, etc.)""" + pass + + +class CodeGenerationError(CompilerError): + """Error during code generation""" + pass + + +class UndeclaredVariableError(SemanticError): + """Variable used without declaration""" + + def __init__(self, variable_name: str, line: int, column: int): + message = f"Variable '{variable_name}' is used but not declared" + super().__init__(message, line, column) + + +class TypeMismatchError(SemanticError): + """Type mismatch in operation or assignment""" + + def __init__(self, expected: str, got: str, line: int, column: int): + message = f"Type mismatch: expected {expected}, got {got}" + super().__init__(message, line, column) + + +class ArrayIndexError(SemanticError): + """Invalid array index or dimension""" + + def __init__(self, array_name: str, message: str, line: int, column: int): + full_message = f"Array '{array_name}': {message}" + super().__init__(full_message, line, column) + + +class DuplicateDeclarationError(SemanticError): + """Variable or function declared multiple times""" + + def __init__(self, name: str, line: int, column: int): + message = f"'{name}' is already declared" + super().__init__(message, line, column) + + +class FunctionError(SemanticError): + """Error related to function/procedure""" + + def __init__(self, function_name: str, message: str, line: int, column: int): + full_message = f"Function '{function_name}': {message}" + super().__init__(full_message, line, column) + + +def format_lark_error(error: Exception, source_code: str) -> str: + """ + Format Lark parsing errors into user-friendly messages + + Args: + error: The Lark exception + source_code: The original pseudocode + + Returns: + Formatted error message + """ + from lark.exceptions import ( + UnexpectedToken, + UnexpectedCharacters, + UnexpectedEOF, + ParseError as LarkParseError + ) + + lines = source_code.split('\n') + + if isinstance(error, UnexpectedToken): + line = error.line + column = error.column + expected = error.expected + token = error.token + + # Get the source line + source_line = lines[line - 1] if 0 < line <= len(lines) else "" + + message = f"Unexpected token '{token}'" + if expected: + expected_list = list(expected) # Convert set to list + expected_str = ", ".join(expected_list[:5]) # Show first 5 expected tokens + if len(expected_list) > 5: + expected_str += "..." + message += f" (expected: {expected_str})" + + return ParseError(message, line, column, source_line).format_error() + + elif isinstance(error, UnexpectedCharacters): + line = error.line + column = error.column + source_line = lines[line - 1] if 0 < line <= len(lines) else "" + + message = f"Unexpected character(s)" + return LexerError(message, line, column, source_line).format_error() + + elif isinstance(error, UnexpectedEOF): + # End of file reached unexpectedly + line = len(lines) + message = "Unexpected end of file. Did you forget to close a block (ENDIF, ENDWHILE, etc.)?" + return ParseError(message, line).format_error() + + elif isinstance(error, LarkParseError): + # Generic parse error + message = str(error) + return ParseError(message).format_error() + + else: + # Unknown error + return f"Compilation error: {str(error)}" + + +def get_error_suggestions(error: CompilerError) -> list: + """ + Get helpful suggestions based on the error type + + Args: + error: The compiler error + + Returns: + List of suggestion strings + """ + suggestions = [] + + error_msg = error.message.lower() + + # Common mistakes and suggestions + if "unexpected token" in error_msg: + suggestions.append("Check for missing keywords (THEN, DO, ENDIF, ENDWHILE, etc.)") + suggestions.append("Ensure all parentheses and brackets are balanced") + + if "undeclared" in error_msg or "not declared" in error_msg: + suggestions.append("Use DECLARE to declare variables before using them") + suggestions.append("Check spelling of variable names (pseudocode is case-insensitive)") + + if "type mismatch" in error_msg: + suggestions.append("Check that variables are used consistently with their declared types") + suggestions.append("Ensure operations are valid for the data types involved") + + if "array" in error_msg: + suggestions.append("Arrays in IGCSE pseudocode are 1-indexed (start at 1, not 0)") + suggestions.append("Declare arrays with: DECLARE arr : ARRAY[1:10] OF INTEGER") + + if "end of file" in error_msg or "eof" in error_msg: + suggestions.append("Check that all control structures are properly closed:") + suggestions.append(" - IF must have ENDIF") + suggestions.append(" - WHILE must have ENDWHILE") + suggestions.append(" - FOR must have NEXT") + suggestions.append(" - PROCEDURE must have ENDPROCEDURE") + suggestions.append(" - FUNCTION must have ENDFUNCTION") + suggestions.append(" - CASE must have ENDCASE") + + if "function" in error_msg or "procedure" in error_msg: + suggestions.append("Functions must have a RETURN statement") + suggestions.append("Procedures are called with CALL, functions are used in expressions") + + return suggestions + + +def format_error_with_suggestions(error: CompilerError) -> dict: + """ + Format error as a dictionary with suggestions + + Args: + error: The compiler error + + Returns: + Dictionary with error message and suggestions + """ + suggestions = get_error_suggestions(error) + + return { + "error": error.format_error(), + "line": error.line, + "column": error.column, + "suggestions": suggestions + } diff --git a/backend/apps/api/execution_engine/grammar.py b/backend/apps/api/execution_engine/grammar.py new file mode 100644 index 0000000..0a46fb2 --- /dev/null +++ b/backend/apps/api/execution_engine/grammar.py @@ -0,0 +1,188 @@ +""" +Lark Grammar for IGCSE Pseudocode + +Formal grammar definition using Lark LALR parser for IGCSE pseudocode. +Supports all standard pseudocode constructs with proper operator precedence. +""" + +PSEUDOCODE_GRAMMAR = r""" +?start: program + +program: _NL* (statement _NL+)* statement? + +// Statements +?statement: declaration + | constant_decl + | assignment + | input_stmt + | output_stmt + | if_stmt + | for_loop + | while_loop + | repeat_loop + | function_decl + | procedure_decl + | return_stmt + | call_stmt + +// Declarations +declaration: DECLARE IDENT COLON type_spec +constant_decl: CONSTANT IDENT EQUALS expression + +type_spec: simple_type + | array_type + +simple_type: INTEGER | REAL | STRING | BOOLEAN | CHAR | DATE + +array_type: ARRAY LBRACK array_dim (COMMA array_dim)* RBRACK OF simple_type + +array_dim: NUMBER COLON NUMBER + +// Expressions (precedence from low to high) +?expression: or_expr + +?or_expr: and_expr (OR and_expr)* + +?and_expr: not_expr (AND not_expr)* + +?not_expr: NOT not_expr -> not_op + | comparison + +?comparison: add_expr (comp_op add_expr)? + +comp_op: EQUALS | NEQ | LEQ | GEQ | LT | GT + +?add_expr: mul_expr ((PLUS|MINUS) mul_expr)* + +?mul_expr: power_expr ((STAR|SLASH|DIV|MOD) power_expr)* + +?power_expr: unary_expr (POWER unary_expr)* + +?unary_expr: MINUS unary_expr -> neg + | PLUS unary_expr -> pos + | atom + +?atom: NUMBER -> number + | STRING_LIT -> string + | TRUE -> true + | FALSE -> false + | func_call + | arr_access + | IDENT -> ident + | LPAR expression RPAR + +// Function calls and array access +func_call: IDENT LPAR [expression (COMMA expression)*] RPAR +arr_access: IDENT LBRACK expression (COMMA expression)* RBRACK + +// Statements +assignment: (IDENT | arr_access) (EQUALS | ARROW) expression + +input_stmt: INPUT (IDENT | arr_access) + +output_stmt: (OUTPUT | PRINT) expression (COMMA expression)* + +// Control flow +if_stmt: IF expression THEN _NL+ (statement _NL+)* elif_part* else_part? ENDIF + +elif_part: ELSEIF expression THEN _NL+ (statement _NL+)* + +else_part: ELSE _NL+ (statement _NL+)* + +for_loop: FOR IDENT EQUALS expression TO expression (STEP expression)? _NL+ (statement _NL+)* NEXT IDENT + +while_loop: WHILE expression DO _NL+ (statement _NL+)* ENDWHILE + +repeat_loop: REPEAT _NL+ (statement _NL+)* UNTIL expression + +// Functions and procedures +procedure_decl: PROCEDURE IDENT LPAR [param_list] RPAR _NL+ (statement _NL+)* ENDPROCEDURE + +function_decl: FUNCTION IDENT LPAR [param_list] RPAR RETURNS simple_type _NL+ (statement _NL+)* ENDFUNCTION + +param_list: parameter (COMMA parameter)* + +parameter: (BYREF | BYVAL)? IDENT COLON type_spec + +return_stmt: RETURN expression + +call_stmt: CALL IDENT LPAR [expression (COMMA expression)*] RPAR + +// Terminals (case-insensitive keywords with priority) +DECLARE.2: /DECLARE/i +CONSTANT.2: /CONSTANT/i +INTEGER.2: /INTEGER/i +REAL.2: /REAL/i +STRING.2: /STRING/i +BOOLEAN.2: /BOOLEAN/i +CHAR.2: /CHAR/i +DATE.2: /DATE/i +ARRAY.2: /ARRAY/i +OF.2: /OF/i +INPUT.2: /INPUT/i +OUTPUT.2: /OUTPUT/i +PRINT.2: /PRINT/i +IF.2: /IF/i +THEN.2: /THEN/i +ELSEIF.2: /ELSEIF/i +ELSE.2: /ELSE/i +ENDIF.2: /ENDIF/i +FOR.2: /FOR/i +TO.2: /TO/i +STEP.2: /STEP/i +NEXT.2: /NEXT/i +WHILE.2: /WHILE/i +DO.2: /DO/i +ENDWHILE.2: /ENDWHILE/i +REPEAT.2: /REPEAT/i +UNTIL.2: /UNTIL/i +PROCEDURE.2: /PROCEDURE/i +ENDPROCEDURE.2: /ENDPROCEDURE/i +FUNCTION.2: /FUNCTION/i +ENDFUNCTION.2: /ENDFUNCTION/i +RETURNS.2: /RETURNS/i +RETURN.2: /RETURN/i +CALL.2: /CALL/i +BYREF.2: /BYREF/i +BYVAL.2: /BYVAL/i +AND.2: /AND/i +OR.2: /OR/i +NOT.2: /NOT/i +DIV.2: /DIV/i +MOD.2: /MOD/i +TRUE.2: /TRUE/i +FALSE.2: /FALSE/i + +// Operators and punctuation +ARROW: "<-" +NEQ: "<>" | "><" +LEQ: "<=" +GEQ: ">=" +EQUALS: "=" +LT: "<" +GT: ">" +PLUS: "+" +MINUS: "-" +STAR: "*" +SLASH: "/" +POWER: "^" +LPAR: "(" +RPAR: ")" +LBRACK: "[" +RBRACK: "]" +COMMA: "," +COLON: ":" + +// Identifiers, numbers, strings +IDENT: /[a-zA-Z_][a-zA-Z0-9_]*/ +NUMBER: /\d+(\.\d+)?/ +STRING_LIT: /"[^"]*"/ | /'[^']*'/ + +// Whitespace and comments +COMMENT: /\/\/[^\n]*/ +_NL: /\r?\n/+ + +%import common.WS_INLINE +%ignore WS_INLINE +%ignore COMMENT +""" diff --git a/backend/apps/api/execution_engine/parser.py b/backend/apps/api/execution_engine/parser.py deleted file mode 100644 index 6e018e0..0000000 --- a/backend/apps/api/execution_engine/parser.py +++ /dev/null @@ -1,755 +0,0 @@ -import os -from dataclasses import dataclass -import re -from typing import List, Optional, Dict, Set, Tuple - -@dataclass -class CodeState: - indent_level: int = 0 - -class PseudocodeConverter: - OPERATORS_MAPPING = { - 'MOD': '%', - 'DIV': '//', - '<>': '!=', - '><': '!=', - '^': '**', - 'OR': 'or', - 'AND': 'and', - 'NOT': 'not', - } - - BUILTIN_MAPPINGS = { - 'random': 'random.random', - 'INT': 'int', - 'LENGTH': 'len', - 'length': 'len', - 'LCASE': 'LCASE', - 'UCASE': 'UCASE', - 'SUBSTRING': 'SUBSTRING', - 'ROUND': 'round' - } - - def __init__(self): - self.state = CodeState() - self.output_lines = [ - "import random", - "import math", - "", - "# Helper class for 1-indexed array implementation", - "class Array(dict):", - " def __init__(self, *args, **kwargs):", - " super().__init__(*args, **kwargs)", - "", - " def __getitem__(self, key):", - " if isinstance(key, tuple):", - " # Handle multi-dimensional access", - " return super().__getitem__(key)", - " return super().__getitem__(key)", - "", - " def __setitem__(self, key, value):", - " super().__setitem__(key, value)", - "", - "def init_array(values=None, dimensions=None):", - " \"\"\"Initialize a 1-indexed array\"\"\"", - " array = Array()", - " if values is not None:", - " # If initializing with list values, convert to 1-indexed dictionary", - " if isinstance(values, list):", - " for i, value in enumerate(values, 1): # Start indexing at 1", - " array[i] = value", - " return array", - " return array", - "", - "def LCASE(s):", - " return s.lower()", - "", - "def UCASE(s):", - " return s.upper()", - "", - "def SUBSTRING(s, start, length):", - " # Adjust for 1-based indexing", - " return s[start-1:start-1+length]", - "", - "# Start of Main Program", - ] - self.array_declarations: Set[str] = set() - self.explicit_arrays: Dict[str, bool] = {} # Tracks arrays with explicit initialization - - def preprocess_code(self, lines: List[str]) -> List[str]: - """ - Preprocesses the input pseudocode by: - 1. Removing empty lines - 2. Removing comments (lines starting with //) - 3. Removing inline comments (anything after // on a line) while preserving string literals - 4. Stripping whitespace - - Args: - lines: The original pseudocode lines - - Returns: - A cleaned list of pseudocode lines - """ - processed_lines = [] - - for line in lines: - # Skip empty lines and comment-only lines - if not line.strip() or line.strip().startswith('//'): - continue - - # Handle inline comments while preserving string literals - result_line = "" - i = 0 - in_string = False - string_char = None - - while i < len(line): - # Check for string boundaries - if line[i] in ('"', "'") and (i == 0 or line[i-1] != '\\'): - if not in_string: - in_string = True - string_char = line[i] - elif line[i] == string_char: - in_string = False - - # Check for comment start but only if we're not inside a string - if i < len(line) - 1 and line[i:i+2] == '//' and not in_string: - break # Found a comment start outside of strings, stop processing - - result_line += line[i] - i += 1 - - # Strip whitespace and add to processed lines - result_line = result_line.strip() - if result_line: - processed_lines.append(result_line) - - return processed_lines - - def insensitive_replace(self, text: str, old: str, new: str) -> str: - """ - Replaces occurrences of 'old' with 'new' in 'text', case-insensitively, - but preserves text within string literals. - """ - result = "" - i = 0 - in_string = False - string_char = None - - while i < len(text): - # Check for string boundaries - if text[i] in ('"', "'") and (i == 0 or text[i-1] != '\\'): - if not in_string: - in_string = True - string_char = text[i] - result += text[i] - elif text[i] == string_char: - in_string = False - result += text[i] - else: - result += text[i] - i += 1 - continue - - # If we're inside a string, add the character as-is - if in_string: - result += text[i] - i += 1 - continue - - # If we're not in a string and we find the pattern, replace it - if i + len(old) <= len(text) and text[i:i+len(old)].upper() == old.upper(): - result += new - i += len(old) - else: - result += text[i] - i += 1 - - return result - - def handle_string_concatenation(self, expression: str) -> str: - """ - Detects plus operators between a string literal and a numeric expression, - and wraps the numeric expression with str() to avoid type errors. - This is a simple heuristic; more robust handling might require proper parsing. - """ - # First, identify string boundaries to avoid incorrect parsing - string_ranges = self.find_string_ranges(expression) - - # If no string literals or no plus operators, return as-is - if not string_ranges or '+' not in expression: - return expression - - # Process the expression carefully to avoid modifying string contents - result = "" - i = 0 - while i < len(expression): - # Check if current position is inside a string - in_string = any(start <= i <= end for start, end in string_ranges) - - # If not in string and we find a '+', analyze context - if not in_string and expression[i] == '+': - # Find left and right operands - left_end = i - right_start = i + 1 - - # Capture left operand - left_operand = expression[:left_end].strip() - # Capture right operand - right_operand = expression[right_start:].strip() - - # Check if either operand is a string literal - left_is_string = left_operand and (left_operand[0] in ('"', "'")) - right_is_string = right_operand and (right_operand[0] in ('"', "'")) - - if left_is_string and not right_is_string and right_operand: - # String + non-string: wrap right with str() - if not right_operand.startswith('str('): - result += f"{left_operand} + str({right_operand})" - i = len(expression) # Skip to end as we've handled everything - else: - result += expression[i] - i += 1 - elif not left_is_string and right_is_string and left_operand: - # Non-string + string: wrap left with str() - if not left_operand.startswith('str('): - result = f"str({left_operand}) + {right_operand}" - i = len(expression) # Skip to end as we've handled everything - else: - result += expression[i] - i += 1 - else: - result += expression[i] - i += 1 - else: - result += expression[i] - i += 1 - - return result - - def find_string_ranges(self, text: str) -> List[Tuple[int, int]]: - """ - Finds the start and end indices of all string literals in the text. - Returns a list of tuples (start, end) marking the boundaries (inclusive). - """ - ranges = [] - i = 0 - in_string = False - string_char = None - start_index = -1 - - while i < len(text): - # Check for string boundaries - if text[i] in ('"', "'") and (i == 0 or text[i-1] != '\\'): - if not in_string: - in_string = True - string_char = text[i] - start_index = i - elif text[i] == string_char: - in_string = False - ranges.append((start_index, i)) - i += 1 - - return ranges - - def convert_array_access(self, expr: str) -> str: - """ - Converts array access notation in expressions, preserving 1-indexed access. - This handles both simple array[index] and 2D array[row,col] notations. - """ - # Pattern for array access with comma-separated indices (2D arrays) - pattern_2d = r'(\w+)\[([^,\]]+),([^,\]]+)\]' - # Replace 2D array access with tuple key format - while re.search(pattern_2d, expr): - expr = re.sub(pattern_2d, r'\1[(\2, \3)]', expr) - - # Pattern for simple array access (1D arrays) - pattern_1d = r'(\w+)\[([^\]]+)\]' - # No adjustment needed as we're using the Array class for 1-indexed access - - return expr - - def convert_array_initialization(self, expr: str) -> str: - """Converts array initialization to use our custom init_array function.""" - if expr.strip().startswith('[') and expr.strip().endswith(']'): - return f"init_array({expr})" - return expr - - def convert_condition(self, statement: str) -> str: - """Converts pseudocode conditional statements to Python syntax.""" - statement = re.sub(r'\bthen\b', '', statement, flags=re.IGNORECASE).strip() - - result = statement - for old, new in self.OPERATORS_MAPPING.items(): - result = self.insensitive_replace(result, old, new) - - # In a condition context, we need to convert '=' to '==' - result = self.replace_equality_operator(result) - - # Handle array access in conditions - result = self.convert_array_access(result) - result = self.evaluate_expression(result, is_condition=True) - - return result - - def replace_equality_operator(self, text: str) -> str: - """ - Replaces '=' with '==' in conditions, but only outside of string literals. - Uses a two-phase approach to ensure accurate string boundary detection. - """ - # First find all string ranges - string_ranges = self.find_string_ranges(text) - - # Then process the text, making replacements only outside string ranges - result = "" - i = 0 - while i < len(text): - # Check if current position is inside any string - in_string = any(start <= i <= end for start, end in string_ranges) - - # Replace standalone '=' with '==' but only if not in a string - if (not in_string and text[i] == '=' and - (i == 0 or text[i-1] not in '!<>=') and - (i == len(text)-1 or text[i+1] != '=')): - result += '==' - else: - result += text[i] - i += 1 - - return result - - def evaluate_expression(self, statement: str, is_condition=False) -> str: - """ - Evaluates and converts pseudocode expressions to Python syntax. - - Args: - statement: The pseudocode expression to convert - is_condition: Whether this expression is in a condition context (if/while) - """ - # First find all string literal ranges - string_ranges = self.find_string_ranges(statement) - - # Apply operator mappings (DIV, MOD, etc.) - result = statement - for old, new in self.OPERATORS_MAPPING.items(): - result = self.insensitive_replace(result, old, new) - - # Apply built-in function mappings - for old, new in self.BUILTIN_MAPPINGS.items(): - result = self.insensitive_replace(result, old, new) - - # Handle array access - result = self.convert_array_access(result) - - # Only convert equality operators in condition contexts - if is_condition: - result = self.replace_equality_operator(result) - - # Handle cases where '+' is used between strings and numbers - result = self.handle_string_concatenation(result) - - # Handle array initialization with square brackets - if '[' in result and ']' in result and '=' in result: - # Find string literals first - string_ranges = self.find_string_ranges(result) - - # Find the assignment operator outside of strings - equals_pos = -1 - i = 0 - while i < len(result): - if any(start <= i <= end for start, end in string_ranges): - i += 1 - continue - - if result[i] == '=' and (i == 0 or result[i-1] != '=') and (i == len(result)-1 or result[i+1] != '='): - equals_pos = i - break - i += 1 - - if equals_pos != -1: - lhs = result[:equals_pos].strip() - rhs = result[equals_pos+1:].strip() - - # Check if the RHS is an array literal outside of strings - if rhs.startswith('[') and rhs.endswith(']'): - # Make sure the '[' and ']' are not inside strings - if not any(start <= rhs.find('[') <= end for start, end in string_ranges) and \ - not any(start <= rhs.rfind(']') <= end for start, end in string_ranges): - result = f"{lhs} = init_array({rhs})" - - return result - - def parse_for_loop(self, line: str) -> Tuple[str, str, str, Optional[str]]: - """ - Parse FOR loop components: "FOR TO STEP " - STEP clause is optional. - """ - pattern = r"FOR\s+(\w+)\s*[←=]\s*(.+?)\s+TO\s+(.+?)(?:\s+STEP\s+(.+))?$" - match = re.match(pattern, line, re.IGNORECASE) - if not match: - raise ValueError(f"Invalid FOR loop syntax: {line}") - var, start, end, step = match.groups() - return var, start.strip(), end.strip(), step.strip() if step else None - - def process_input_line(self, line: str) -> Optional[str]: - """Processes a single line of pseudocode and returns Python equivalent.""" - line = line.strip() - if not line or line.startswith('//'): - return None - - indent = " " * self.state.indent_level - upper_line = line.upper() - - if upper_line.startswith('PROCEDURE'): - return self.handle_procedure(line, indent) - elif upper_line.startswith('FUNCTION'): - return self.handle_function(line, indent) - elif upper_line.startswith('RETURN'): - return self.handle_return(line, indent) - elif upper_line.startswith('DECLARE'): - return self.handle_declaration(line, indent) - elif upper_line.startswith('CONSTANT'): - return self.handle_constant(line, indent) - elif upper_line.startswith('CALL'): - return self.handle_call(line, indent) - elif upper_line.startswith('WHILE'): - return self.handle_while(line, indent) - elif upper_line.startswith('IF'): - return self.handle_if(line, indent) - elif upper_line.startswith('ELSE'): - return self.handle_else(line, indent) - elif upper_line.startswith('FOR'): - return self.handle_for(line, indent) - elif re.search(r"\b(ENDWHILE|ENDIF|NEXT|ENDFUNCTION|ENDPROCEDURE)\b", upper_line): - self.state.indent_level -= 4 - return None - elif upper_line.startswith('PRINT'): - return self.handle_print(line, indent) - elif upper_line.startswith('OUTPUT'): - return self.handle_output(line, indent) - elif upper_line.startswith('INPUT'): - return self.handle_input(line, indent) - elif '=' in line and '[' in line: - return self.handle_array_initialization(line, indent) - elif '=' in line: - # This is a regular assignment, not a condition - return f"{indent}{self.evaluate_expression(line, is_condition=False)}" - return None - - - def handle_procedure(self, line: str, indent: str) -> str: - """Converts a PROCEDURE definition to a Python function.""" - match = re.match(r'PROCEDURE\s+(\w+)\((.*?)\)', line, re.IGNORECASE) - if match: - proc_name, params = match.groups() - param_list = [] - for param in params.split(','): - param_name = param.split(':')[0].strip() - param_list.append(param_name) - params_str = ", ".join(param_list) - self.state.indent_level += 4 - return f"{indent}def {proc_name}({params_str}):" - else: - match = re.match(r'PROCEDURE\s+(\w+)', line, re.IGNORECASE) - if match: - proc_name = match.group(1) - self.state.indent_level += 4 - return f"{indent}def {proc_name}():" - else: - raise ValueError(f"Invalid PROCEDURE syntax: {line}") - - - def handle_function(self, line: str, indent: str) -> str: - """Converts a FUNCTION definition to a Python function.""" - match = re.match(r"FUNCTION\s+(\w+)\s*\((.*?)\)\s+RETURNS\s+(\w+)", line, re.IGNORECASE) - if match: - func_name, params, ret_type = match.groups() - param_list = [] - for param in params.split(','): - if param.strip(): - param_name = param.split(':')[0].strip() - param_list.append(param_name) - params_str = ", ".join(param_list) - self.state.indent_level += 4 - return f"{indent}def {func_name}({params_str}): # Returns {ret_type}" - else: - match = re.match(r"FUNCTION\s+(\w+)\s+RETURNS\s+(\w+)", line, re.IGNORECASE) - if match: - func_name, ret_type = match.groups() - self.state.indent_level += 4 - return f"{indent}def {func_name}(): # Returns {ret_type}" - else: - raise ValueError(f"Invalid FUNCTION syntax: {line}") - - - def handle_return(self, line: str, indent: str) -> str: - """Converts a RETURN statement.""" - expr = line[len("RETURN"):].strip() - expr = self.evaluate_expression(expr) - return f"{indent}return {expr}" - - - def handle_declaration(self, line: str, indent: str) -> str: - """Converts a DECLARE statement for scalars or arrays.""" - upper_line = line.upper() - if 'ARRAY' in upper_line: - pattern = r"DECLARE\s+(\w+)\s*:\s*ARRAY\[(.*?)\]\s+OF\s+(\w+)" - match = re.match(pattern, line, re.IGNORECASE) - if match: - var_name, dims, type_name = match.groups() - dims = dims.strip() - - # Add to our explicit arrays tracking - self.explicit_arrays[var_name] = True - - # Process dimensions for 2D arrays - dim_parts = dims.split(',') - if len(dim_parts) == 2: - # Handle 2D array with format like "1:10, 1:5" - dim_init_args = [] - for dim_part in dim_parts: - bounds = dim_part.split(':') - if len(bounds) == 2: - dim_init_args.append(int(bounds[0].strip())) - dim_init_args.append(int(bounds[1].strip())) - - if len(dim_init_args) == 4: - # Format is min_row:max_row, min_col:max_col - return f"{indent}{var_name} = init_array(dimensions=({dim_init_args[0]}, {dim_init_args[1]}, {dim_init_args[2]}, {dim_init_args[3]})) # 2D Array with dimensions [{dims}] of type {type_name}" - - # Initialize as our custom Array type (default case) - return f"{indent}{var_name} = init_array() # Array with dimensions [{dims}] of type {type_name}" - else: - raise ValueError(f"Invalid DECLARE ARRAY syntax: {line}") - else: - pattern = r"DECLARE\s+(\w+)\s*:\s*(\w+)" - match = re.match(pattern, line, re.IGNORECASE) - if match: - var_name, type_name = match.groups() - return f"{indent}{var_name} = None # Declared as {type_name}" - else: - raise ValueError(f"Invalid DECLARE syntax: {line}") - - - def handle_constant(self, line: str, indent: str) -> str: - """Converts a CONSTANT declaration.""" - pattern = r"CONSTANT\s+(\w+)\s*=\s*(.+)" - match = re.match(pattern, line, re.IGNORECASE) - if match: - var_name, value = match.groups() - return f"{indent}{var_name} = {value}" - else: - raise ValueError(f"Invalid CONSTANT syntax: {line}") - - - def handle_call(self, line: str, indent: str) -> str: - """Converts a CALL statement to a function call.""" - call_content = line[4:].strip() - if '(' in call_content and call_content.endswith(')'): - proc_name = call_content[:call_content.find('(')].strip() - params = call_content[call_content.find('(')+1: call_content.rfind(')')].strip() - params_eval = self.evaluate_expression(params) - return f"{indent}{proc_name}({params_eval})" - else: - proc_name = call_content.strip() - return f"{indent}{proc_name}()" - - - def handle_array_initialization(self, line: str, indent: str) -> str: - """Handles explicit array initialization lines.""" - var_name = line[:line.find('=')].strip() - # If the LHS contains an array access, then simply evaluate the expression - if '[' in var_name: - return f"{indent}{self.evaluate_expression(line)}" - - value = line[line.find('=')+1:].strip() - - # Mark this as an explicit array - if '[' in value: - self.explicit_arrays[var_name] = True - - # If it's a standard-looking array initialization, use our init_array function - if value.startswith('[') and value.endswith(']'): - # Check if it's a 2D array initialization by looking for nested lists - if re.search(r'\[\s*\[', value): - # This is likely a 2D array initialization like [[1,2], [3,4]] - return f"{indent}{var_name} = init_array({value})" - else: - # This is a 1D array initialization - return f"{indent}{var_name} = init_array({value})" - - return f"{indent}{self.evaluate_expression(line)}" - - - def handle_while(self, line: str, indent: str) -> str: - """Converts a WHILE loop.""" - self.state.indent_level += 4 - condition = line[5:].split('DO')[0].strip() - return f"{indent}while {self.convert_condition(condition)}:" - - - def handle_if(self, line: str, indent: str) -> str: - """Converts an IF statement.""" - self.state.indent_level += 4 - condition = line[2:].strip() - if 'THEN' in condition.upper(): - condition = condition[:condition.upper().find('THEN')].strip() - converted_condition = self.convert_condition(condition) - return f"{indent}if {converted_condition}:" - - - def handle_else(self, line: str, indent: str) -> str: - """Converts an ELSE or ELSE IF statement.""" - self.state.indent_level -= 4 - indent = " " * self.state.indent_level - self.state.indent_level += 4 - upper_line = line.upper() - if 'IF' in upper_line: - # For ELSE IF, skip the "ELSE " portion (7 characters) - condition = line[7:].strip() - if 'THEN' in condition.upper(): - condition = condition[:condition.upper().find('THEN')].strip() - return f"{indent}elif {self.convert_condition(condition)}:" - return f"{indent}else:" - - - def handle_for(self, line: str, indent: str) -> str: - """Converts a FOR loop.""" - self.state.indent_level += 4 - var, start, end, step = self.parse_for_loop(line) - if step: - return f"{indent}for {var} in range({start}, ({end})+1, {step}):" - else: - return f"{indent}for {var} in range({start}, ({end})+1):" - - - def handle_print(self, line: str, indent: str) -> str: - """Converts a PRINT statement.""" - content = line[5:].strip() - if content == '': - return f"{indent}print()" - content = self.evaluate_expression(content) - return f"{indent}print({content})" - - - def handle_output(self, line: str, indent: str) -> str: - """Converts an OUTPUT statement.""" - content = line[6:].strip() - if content == '': - return f"{indent}print('')" - content = self.evaluate_expression(content) - return f"{indent}print({content})" - - - def handle_input(self, line: str, indent: str) -> str: - """Converts an INPUT statement.""" - content = line[5:].strip() - parts = content.rsplit(maxsplit=1) - if len(parts) == 2: - prompt_expr, var = parts - prompt_expr_evaluated = self.evaluate_expression(prompt_expr) - return f"{indent}{var} = eval(input({prompt_expr_evaluated}))" - else: - if content and content[0] in ('"', "'"): - quote_char = content[0] - end_quote_index = content.find(quote_char, 1) - if end_quote_index == -1: - raise ValueError("INPUT prompt string not terminated") - prompt = content[:end_quote_index+1] - var = content[end_quote_index+1:].strip() - return f"{indent}{var} = eval(input({prompt}))" - else: - var = content - return f"{indent}{var} = eval(input())" - - def find_arrays(self, lines: List[str]) -> None: - """ - Identifies arrays used in the code and their dimensions. - - Explicit array declarations (via DECLARE or assignment statement using [ ) - are flagged as "explicit" while implicit accesses are captured separately. - Multi-dimensional access (e.g., arr[i][j]) is partially handled by a simple regex. - """ - for line in lines: - stripped = line.strip() - upper_line = stripped.upper() - - # Process explicit array declarations - if upper_line.startswith("DECLARE") and "ARRAY" in upper_line: - pattern = r"DECLARE\s+(\w+)\s*:\s*ARRAY\[(.*?)\]\s+OF\s+(\w+)" - match = re.match(pattern, stripped, re.IGNORECASE) - if match: - var_name, dims, type_name = match.groups() - dims = dims.strip() - # Flag as explicitly declared (could also store dims/type if needed) - self.explicit_arrays[var_name] = True - continue - - # Process assignment lines for explicit array initialization - if '=' in line: - parts = line.split('=') - lhs = parts[0].strip() - rhs = parts[1].strip() - # If the RHS starts with '[' (suggesting explicit initialization), mark it. - if rhs.startswith('['): - self.explicit_arrays[lhs] = True - continue - - # Process implicit array accesses: - # The regex handles single or multi-dimensional array accesses (e.g., arr[ or matrix[) - # by matching the first occurrence of an identifier followed by '['. - for match in re.findall(r"(\w+)\s*\[", line): - if match not in self.explicit_arrays: - self.array_declarations.add(match) - self.explicit_arrays[match] = False - - def generate_array_initializations(self) -> List[str]: - """ - Generates initialization code for arrays that were accessed implicitly. - All arrays are initialized as our custom Array class. - """ - result = [] - for name in self.array_declarations: - # Only auto-initialize if not explicitly declared/initialized. - if name in self.explicit_arrays and self.explicit_arrays[name]: - continue - result.append(f"{name} = init_array()") - return result - - def convert(self, lines: List[str]) -> List[str]: - """Converts pseudocode lines to Python and executes it.""" - - # Preprocess the code to remove comments and empty lines - cleaned_lines = self.preprocess_code(lines) - - self.find_arrays(cleaned_lines) - array_inits = self.generate_array_initializations() - if array_inits: - self.output_lines.extend(array_inits) - - for line in cleaned_lines: - # Skip lines that are just array declarations we've already handled - if '=' in line and any(line.strip().startswith(arr_name) for arr_name in self.array_declarations): - continue - - result = self.process_input_line(line) - if result: - self.output_lines.append(result) - - return self.output_lines - -def main(): - base_path = os.path.dirname(os.path.abspath(__file__)) - input_path = os.path.join(base_path, "input.txt") - output_path = os.path.join(base_path, "output.py") - - converter = PseudocodeConverter() - with open(input_path, 'r') as file: - lines = file.readlines() - converted_lines = converter.convert(lines) - - with open(output_path, 'w') as file: - file.write('\n'.join(converted_lines)) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/backend/apps/api/execution_engine/views.py b/backend/apps/api/execution_engine/views.py index bce8703..89cebbc 100644 --- a/backend/apps/api/execution_engine/views.py +++ b/backend/apps/api/execution_engine/views.py @@ -1,23 +1,63 @@ from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt -from .parser import PseudocodeConverter # Correct import path +from .compiler import PseudocodeCompiler +from .errors import CompilerError import json @csrf_exempt def execute_code(request): + """ + API endpoint to compile IGCSE pseudocode to Python + + POST /execution/convert/ + Body: {"pseudocode": "..."} + + Returns: + Success: {"python_code": "...", "success": true} + Error: {"error": "...", "suggestions": [...], "line": ..., "success": false} + """ if request.method == 'POST': try: data = json.loads(request.body) pseudocode = data.get('pseudocode', '') + if not pseudocode: - return JsonResponse({'error': 'No pseudocode provided'}, status=400) + return JsonResponse({ + 'error': 'No pseudocode provided', + 'success': False + }, status=400) - converter = PseudocodeConverter() - pseudocode_lines = pseudocode.split('\n') - python_code = converter.convert(pseudocode_lines) - python_code_str = '\n'.join(python_code) + # Use the new compiler with permissive mode for better compatibility + compiler = PseudocodeCompiler(permissive=True) + result = compiler.compile_with_errors(pseudocode) - return JsonResponse({'python_code': python_code_str}, status=200) + if result['success']: + return JsonResponse({ + 'python_code': result['python_code'], + 'success': True + }, status=200) + else: + # Return detailed error information + return JsonResponse({ + 'error': result['error'], + 'suggestions': result.get('suggestions', []), + 'line': result.get('line'), + 'column': result.get('column'), + 'success': False + }, status=400) + + except json.JSONDecodeError: + return JsonResponse({ + 'error': 'Invalid JSON in request body', + 'success': False + }, status=400) except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - return JsonResponse({'error': 'Invalid request method'}, status=405) \ No newline at end of file + return JsonResponse({ + 'error': f'Unexpected server error: {str(e)}', + 'success': False + }, status=500) + + return JsonResponse({ + 'error': 'Invalid request method. Use POST.', + 'success': False + }, status=405) \ No newline at end of file diff --git a/backend/fix_positions.py b/backend/fix_positions.py new file mode 100644 index 0000000..83914c2 --- /dev/null +++ b/backend/fix_positions.py @@ -0,0 +1,79 @@ +""" +Script to automatically add line/column extraction to all transformer methods +""" + +import re + +compiler_file = 'apps/api/execution_engine/compiler.py' + +with open(compiler_file, 'r') as f: + content = f.read() + +# Pattern to find method definitions that create AST nodes with line=1, column=1 +# We need to: +# 1. Find each method that has "line=1, column=1" +# 2. Add "line, column = self._get_position(items)" at the start +# 3. Replace "line=1, column=1" with "line=line, column=column" + +# Split into lines for easier processing +lines = content.split('\n') +result_lines = [] +i = 0 + +while i < len(lines): + line = lines[i] + + # Check if this is a method definition (def ...) + if line.strip().startswith('def ') and not line.strip().startswith('def _get_position'): + # Check if this method or upcoming lines have "line=1, column=1" + # Look ahead up to 30 lines + has_hardcoded_pos = False + for j in range(i, min(i + 30, len(lines))): + if 'line=1, column=1' in lines[j]: + has_hardcoded_pos = True + break + # Stop at next method definition + if j > i and lines[j].strip().startswith('def '): + break + + result_lines.append(line) # Add the def line + i += 1 + + if has_hardcoded_pos: + # Add the docstring line (if present) + if i < len(lines) and ('"""' in lines[i] or "'''" in lines[i]): + result_lines.append(lines[i]) + i += 1 + # Multi-line docstring - continue until closing quotes + while i < len(lines) and not (lines[i].strip().endswith('"""') or lines[i].strip().endswith("'''")): + result_lines.append(lines[i]) + i += 1 + if i < len(lines): + result_lines.append(lines[i]) # Closing docstring line + i += 1 + + # Check if next line already has position extraction + if i < len(lines) and 'line, column = self._get_position' not in lines[i]: + # Get the indentation from the next line + next_line = lines[i] if i < len(lines) else '' + indent = len(next_line) - len(next_line.lstrip()) + indent_str = ' ' * indent + + # Add position extraction line + result_lines.append(f'{indent_str}line, column = self._get_position(items)') + else: + result_lines.append(line) + i += 1 + +# Join back together +content = '\n'.join(result_lines) + +# Now replace all "line=1, column=1" with "line=line, column=column" +content = content.replace('line=1, column=1', 'line=line, column=column') + +# Write back +with open(compiler_file, 'w') as f: + f.write(content) + +print("✓ Fixed all hardcoded line/column positions!") +print(f"✓ Updated {compiler_file}") diff --git a/backend/requirements.txt b/backend/requirements.txt index 265545c..666ea8c 100644 Binary files a/backend/requirements.txt and b/backend/requirements.txt differ diff --git a/backend/test_byref.py b/backend/test_byref.py new file mode 100644 index 0000000..a8b494b --- /dev/null +++ b/backend/test_byref.py @@ -0,0 +1,66 @@ +"""Test BYREF parameter support with Swap procedure""" +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'apps', 'api', 'execution_engine')) + +from compiler import PseudocodeCompiler + +compiler = PseudocodeCompiler() + +# Test Swap procedure with BYREF parameters +code = """ +PROCEDURE Swap(BYREF a : INTEGER, BYREF b : INTEGER) + DECLARE temp : INTEGER + temp = a + a = b + b = temp +ENDPROCEDURE + +DECLARE x : INTEGER +DECLARE y : INTEGER +x = 5 +y = 10 +OUTPUT "Before swap: x =", x, "y =", y +CALL Swap(x, y) +OUTPUT "After swap: x =", x, "y =", y +""" + +try: + result = compiler.compile_with_errors(code) +except Exception as e: + import traceback + print("✗ Exception during compilation:") + traceback.print_exc() + sys.exit(1) + +if result['success']: + print("✓ BYREF Compilation successful!") + print("\n===== Generated Python Code =====") + # Print only the main program part (skip runtime library) + lines = result['python_code'].split('\n') + main_start = 0 + for idx, line in enumerate(lines): + if '# ===== Main Program =====' in line: + main_start = idx + break + + # Also print the Swap function + swap_start = 0 + for idx, line in enumerate(lines): + if 'def Swap(' in line: + swap_start = idx - 1 # Include line before def + break + + print('\n'.join(lines[swap_start:])) + + print("\n===== Testing Execution =====") + try: + exec(result['python_code']) + except Exception as e: + print(f"✗ Execution error: {e}") +else: + print("✗ FAILED:", result['error']) + if result.get('suggestions'): + print("\nSuggestions:") + for s in result['suggestions']: + print(f" - {s}") diff --git a/backend/test_compiler.py b/backend/test_compiler.py new file mode 100644 index 0000000..6d69527 --- /dev/null +++ b/backend/test_compiler.py @@ -0,0 +1,167 @@ +""" +Test script for the new IGCSE Pseudocode Compiler +""" + +import sys +import os + +# Add the apps/api/execution_engine directory to Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'apps', 'api', 'execution_engine')) + +from compiler import PseudocodeCompiler + +def test_basic_examples(): + """Test basic pseudocode examples""" + + compiler = PseudocodeCompiler(permissive=True) + + # Test 1: Simple FOR loop + print("=" * 70) + print("TEST 1: Simple FOR Loop") + print("=" * 70) + pseudocode1 = """ +FOR i = 1 TO 5 + OUTPUT i +NEXT i +""" + try: + result = compiler.compile_with_errors(pseudocode1) + if result['success']: + print("✓ Compilation successful!") + print("\nGenerated Python code:") + print(result['python_code']) + else: + print("✗ Compilation failed:") + print(result['error']) + if result.get('suggestions'): + print("\nSuggestions:") + for s in result['suggestions']: + print(f" - {s}") + except Exception as e: + print(f"✗ Exception: {e}") + + # Test 2: IF statement + print("\n" + "=" * 70) + print("TEST 2: IF Statement") + print("=" * 70) + pseudocode2 = """ +DECLARE x : INTEGER +x = 10 +IF x > 5 THEN + OUTPUT "Greater than 5" +ELSE + OUTPUT "Less than or equal to 5" +ENDIF +""" + try: + result = compiler.compile_with_errors(pseudocode2) + if result['success']: + print("✓ Compilation successful!") + print("\nGenerated Python code:") + print(result['python_code']) + else: + print("✗ Compilation failed:") + print(result['error']) + if result.get('suggestions'): + print("\nSuggestions:") + for s in result['suggestions']: + print(f" - {s}") + except Exception as e: + print(f"✗ Exception: {e}") + + # Test 3: WHILE loop + print("\n" + "=" * 70) + print("TEST 3: WHILE Loop") + print("=" * 70) + pseudocode3 = """ +DECLARE count : INTEGER +count = 1 +WHILE count <= 3 DO + OUTPUT count + count = count + 1 +ENDWHILE +""" + try: + result = compiler.compile_with_errors(pseudocode3) + if result['success']: + print("✓ Compilation successful!") + print("\nGenerated Python code:") + print(result['python_code']) + else: + print("✗ Compilation failed:") + print(result['error']) + if result.get('suggestions'): + print("\nSuggestions:") + for s in result['suggestions']: + print(f" - {s}") + except Exception as e: + print(f"✗ Exception: {e}") + + # Test 4: Array declaration + print("\n" + "=" * 70) + print("TEST 4: Array Declaration and Access") + print("=" * 70) + pseudocode4 = """ +DECLARE numbers : ARRAY[1:5] OF INTEGER +FOR i = 1 TO 5 + numbers[i] = i * 2 + OUTPUT numbers[i] +NEXT i +""" + try: + result = compiler.compile_with_errors(pseudocode4) + if result['success']: + print("✓ Compilation successful!") + print("\nGenerated Python code:") + print(result['python_code']) + else: + print("✗ Compilation failed:") + print(result['error']) + if result.get('suggestions'): + print("\nSuggestions:") + for s in result['suggestions']: + print(f" - {s}") + except Exception as e: + print(f"✗ Exception: {e}") + + # Test 5: Function + print("\n" + "=" * 70) + print("TEST 5: Function Declaration") + print("=" * 70) + pseudocode5 = """ +FUNCTION Add(a : INTEGER, b : INTEGER) RETURNS INTEGER + RETURN a + b +ENDFUNCTION + +DECLARE result : INTEGER +result = Add(5, 3) +OUTPUT result +""" + try: + result = compiler.compile_with_errors(pseudocode5) + if result['success']: + print("✓ Compilation successful!") + print("\nGenerated Python code:") + print(result['python_code']) + else: + print("✗ Compilation failed:") + print(result['error']) + if result.get('suggestions'): + print("\nSuggestions:") + for s in result['suggestions']: + print(f" - {s}") + except Exception as e: + print(f"✗ Exception: {e}") + +if __name__ == '__main__': + print("\n") + print("*" * 70) + print("*" + " " * 14 + "IGCSE PSEUDOCODE COMPILER TEST SUITE" + " " * 18 + "*") + print("*" * 70) + print("\n") + + test_basic_examples() + + print("\n" + "=" * 70) + print("TESTS COMPLETED") + print("=" * 70) diff --git a/backend/test_examples.py b/backend/test_examples.py new file mode 100644 index 0000000..849871b --- /dev/null +++ b/backend/test_examples.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +""" +Test script to verify that all examples from examplePicker.tsx compile successfully +""" + +import sys +import os + +# Add the execution_engine directory to Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'apps', 'api', 'execution_engine')) + +from compiler import PseudocodeCompiler +from errors import CompilerError + +# All examples from examplePicker.tsx +examples = [ + ("Hello World", '''OUTPUT "Hello, World!"'''), + + ("Variables and Assignment", '''DECLARE x : INTEGER +DECLARE y : INTEGER +x = 10 +y = 20 +OUTPUT "x = ", x +OUTPUT "y = ", y +OUTPUT "x + y = ", x + y'''), + + ("Constants", '''CONSTANT PI = 3.14159 +DECLARE radius : REAL +radius = 5 +area = PI * radius * radius +OUTPUT "Area of circle: ", area'''), + + ("Simple Input", '''DECLARE name : STRING +INPUT name +OUTPUT "Hello, ", name'''), + + ("Multiple Inputs", '''DECLARE length : REAL +DECLARE width : REAL +OUTPUT "Enter length: " +INPUT length +OUTPUT "Enter width: " +INPUT width +area = length * width +OUTPUT "Area = ", area'''), + + ("IF Statement", '''DECLARE age : INTEGER +INPUT age +IF age >= 18 THEN + OUTPUT "You are an adult" +ELSE + OUTPUT "You are a minor" +ENDIF'''), + + ("Nested IF", '''DECLARE score : INTEGER +INPUT score +IF score >= 90 THEN + OUTPUT "Grade: A" +ELSEIF score >= 80 THEN + OUTPUT "Grade: B" +ELSEIF score >= 70 THEN + OUTPUT "Grade: C" +ELSEIF score >= 60 THEN + OUTPUT "Grade: D" +ELSE + OUTPUT "Grade: F" +ENDIF'''), + + ("FOR Loop", '''DECLARE i : INTEGER +FOR i = 1 TO 10 + OUTPUT i +NEXT i'''), + + ("FOR Loop with STEP", '''DECLARE i : INTEGER +FOR i = 0 TO 20 STEP 2 + OUTPUT i +NEXT i'''), + + ("WHILE Loop", '''DECLARE count : INTEGER +count = 1 +WHILE count <= 5 DO + OUTPUT count + count = count + 1 +ENDWHILE'''), + + ("REPEAT Loop", '''DECLARE num : INTEGER +num = 1 +REPEAT + OUTPUT num + num = num + 1 +UNTIL num > 5'''), + + ("Nested Loops", '''DECLARE i : INTEGER +DECLARE j : INTEGER +FOR i = 1 TO 3 + FOR j = 1 TO 3 + OUTPUT i, " x ", j, " = ", i * j + NEXT j +NEXT i'''), + + ("1D Array", '''DECLARE numbers : ARRAY[1:5] OF INTEGER +DECLARE i : INTEGER +FOR i = 1 TO 5 + numbers[i] = i * 2 +NEXT i +FOR i = 1 TO 5 + OUTPUT numbers[i] +NEXT i'''), + + ("2D Array", '''DECLARE matrix : ARRAY[1:3, 1:3] OF INTEGER +DECLARE i : INTEGER +DECLARE j : INTEGER +FOR i = 1 TO 3 + FOR j = 1 TO 3 + matrix[i, j] = i * j + NEXT j +NEXT i +FOR i = 1 TO 3 + FOR j = 1 TO 3 + OUTPUT matrix[i, j], " " + NEXT j + OUTPUT "" +NEXT i'''), + + ("Find Maximum in Array", '''DECLARE numbers : ARRAY[1:5] OF INTEGER +DECLARE i : INTEGER +DECLARE max : INTEGER +FOR i = 1 TO 5 + INPUT numbers[i] +NEXT i +max = numbers[1] +FOR i = 2 TO 5 + IF numbers[i] > max THEN + max = numbers[i] + ENDIF +NEXT i +OUTPUT "Maximum: ", max'''), + + ("Simple Procedure", '''PROCEDURE Greet() + OUTPUT "Hello from procedure!" +ENDPROCEDURE + +CALL Greet()'''), + + ("Procedure with Parameters", '''PROCEDURE PrintSum(a : INTEGER, b : INTEGER) + DECLARE sum : INTEGER + sum = a + b + OUTPUT "Sum = ", sum +ENDPROCEDURE + +CALL PrintSum(5, 3) +CALL PrintSum(10, 20)'''), + + ("Procedure with BYREF", '''PROCEDURE Swap(BYREF a : INTEGER, BYREF b : INTEGER) + DECLARE temp : INTEGER + temp = a + a = b + b = temp +ENDPROCEDURE + +DECLARE x : INTEGER +DECLARE y : INTEGER +x = 5 +y = 10 +OUTPUT "Before: x=", x, ", y=", y +CALL Swap(x, y) +OUTPUT "After: x=", x, ", y=", y'''), + + ("Simple Function", '''FUNCTION Square(n : INTEGER) RETURNS INTEGER + RETURN n * n +ENDFUNCTION + +DECLARE result : INTEGER +result = Square(5) +OUTPUT "5 squared = ", result'''), + + ("Function with Multiple Parameters", '''FUNCTION Add(a : INTEGER, b : INTEGER) RETURNS INTEGER + RETURN a + b +ENDFUNCTION + +DECLARE sum : INTEGER +sum = Add(10, 20) +OUTPUT "Sum = ", sum'''), + + ("Factorial Function", '''FUNCTION Factorial(n : INTEGER) RETURNS INTEGER + DECLARE result : INTEGER + DECLARE i : INTEGER + result = 1 + FOR i = 1 TO n + result = result * i + NEXT i + RETURN result +ENDFUNCTION + +DECLARE num : INTEGER +DECLARE fact : INTEGER +num = 5 +fact = Factorial(num) +OUTPUT num, "! = ", fact'''), + + ("Is Prime Function", '''FUNCTION IsPrime(n : INTEGER) RETURNS BOOLEAN + DECLARE i : INTEGER + IF n <= 1 THEN + RETURN FALSE + ENDIF + FOR i = 2 TO n - 1 + IF n MOD i = 0 THEN + RETURN FALSE + ENDIF + NEXT i + RETURN TRUE +ENDFUNCTION + +DECLARE num : INTEGER +num = 17 +IF IsPrime(num) THEN + OUTPUT num, " is prime" +ELSE + OUTPUT num, " is not prime" +ENDIF'''), + + ("String Operations", '''DECLARE text : STRING +text = "Hello" +OUTPUT "Length: ", LENGTH(text) +OUTPUT "Substring: ", SUBSTRING(text, 1, 3)'''), + + ("String Concatenation", '''DECLARE first : STRING +DECLARE last : STRING +DECLARE full : STRING +first = "John" +last = "Doe" +full = first + " " + last +OUTPUT "Full name: ", full'''), + + ("Calculate Average", '''DECLARE numbers : ARRAY[1:5] OF INTEGER +DECLARE i : INTEGER +DECLARE sum : INTEGER +DECLARE average : REAL +sum = 0 +FOR i = 1 TO 5 + OUTPUT "Enter number ", i, ": " + INPUT numbers[i] + sum = sum + numbers[i] +NEXT i +average = sum / 5 +OUTPUT "Average = ", average'''), + + ("Number Guessing Game", '''DECLARE secret : INTEGER +DECLARE guess : INTEGER +DECLARE attempts : INTEGER +secret = 42 +attempts = 0 +OUTPUT "Guess the number (1-100)!" +REPEAT + INPUT guess + attempts = attempts + 1 + IF guess < secret THEN + OUTPUT "Too low!" + ELSEIF guess > secret THEN + OUTPUT "Too high!" + ENDIF +UNTIL guess = secret +OUTPUT "Correct! You got it in ", attempts, " attempts"'''), + + ("Bubble Sort", '''DECLARE arr : ARRAY[1:5] OF INTEGER +DECLARE i : INTEGER +DECLARE j : INTEGER +DECLARE temp : INTEGER +DECLARE swapped : BOOLEAN + +OUTPUT "Enter 5 numbers:" +FOR i = 1 TO 5 + INPUT arr[i] +NEXT i + +FOR i = 1 TO 4 + swapped = FALSE + FOR j = 1 TO 5 - i + IF arr[j] > arr[j + 1] THEN + temp = arr[j] + arr[j] = arr[j + 1] + arr[j + 1] = temp + swapped = TRUE + ENDIF + NEXT j + IF NOT swapped THEN + i = 5 + ENDIF +NEXT i + +OUTPUT "Sorted array:" +FOR i = 1 TO 5 + OUTPUT arr[i] +NEXT i'''), +] + +def test_examples(): + """Test all examples to ensure they compile""" + compiler = PseudocodeCompiler() + + passed = 0 + failed = 0 + errors = [] + + print("Testing all examples from examplePicker.tsx...\n") + print("=" * 70) + + for title, code in examples: + try: + result = compiler.compile(code) + print(f"✓ {title:40s} - Compilation successful") + passed += 1 + except CompilerError as e: + print(f"✗ {title:40s} - Compilation failed") + failed += 1 + errors.append((title, str(e))) + except Exception as e: + print(f"✗ {title:40s} - Unexpected error") + failed += 1 + errors.append((title, f"Unexpected error: {str(e)}")) + + print("=" * 70) + print(f"\nResults: {passed} passed, {failed} failed out of {len(examples)} total\n") + + if errors: + print("Failed examples:") + print("-" * 70) + for title, error in errors: + print(f"\n{title}:") + print(f" {error}") + + return failed == 0 + +if __name__ == "__main__": + success = test_examples() + sys.exit(0 if success else 1) diff --git a/backend/test_for_loop.py b/backend/test_for_loop.py new file mode 100644 index 0000000..b67e7e3 --- /dev/null +++ b/backend/test_for_loop.py @@ -0,0 +1,28 @@ +"""Quick test for FOR loop""" +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'apps', 'api', 'execution_engine')) + +from compiler import PseudocodeCompiler + +compiler = PseudocodeCompiler() +code = """ +FOR i = 1 TO 5 + OUTPUT i +NEXT i +""" + +result = compiler.compile_with_errors(code) +if result['success']: + print("✓ SUCCESS!") + print("\n===== Generated Python Code =====") + # Print only the main program part (skip runtime library) + lines = result['python_code'].split('\n') + main_start = 0 + for idx, line in enumerate(lines): + if '# ===== Main Program =====' in line: + main_start = idx + break + print('\n'.join(lines[main_start:])) +else: + print("✗ FAILED:", result['error']) diff --git a/backend/test_grammar_v2.py b/backend/test_grammar_v2.py new file mode 100644 index 0000000..d905d6a --- /dev/null +++ b/backend/test_grammar_v2.py @@ -0,0 +1,36 @@ +""" +Test the grammar parsing +""" + +from lark import Lark +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'apps', 'api', 'execution_engine')) + +from grammar import PSEUDOCODE_GRAMMAR + +try: + parser = Lark(PSEUDOCODE_GRAMMAR, start='program', parser='lalr') + print("✓ Grammar loaded successfully!") + + # Test simple pseudocode + test_code = """ +OUTPUT 42 +""" + result = parser.parse(test_code) + print(f"✓ Parsed simple code: {result.pretty()}") + + # Test FOR loop + test_code2 = """ +FOR i = 1 TO 5 + OUTPUT i +NEXT i +""" + result2 = parser.parse(test_code2) + print(f"✓ Parsed FOR loop: {result2.pretty()}") + +except Exception as e: + print(f"✗ Error: {e}") + import traceback + traceback.print_exc() diff --git a/backend/test_simple_grammar.py b/backend/test_simple_grammar.py new file mode 100644 index 0000000..b3a22e4 --- /dev/null +++ b/backend/test_simple_grammar.py @@ -0,0 +1,32 @@ +""" +Test a simple Lark grammar to verify basic syntax +""" + +from lark import Lark + +# Very simple test grammar +test_grammar = r""" + start: statement+ + + statement: "OUTPUT"i expression + + expression: NUMBER + | STRING + + NUMBER: /\d+/ + STRING: /"[^"]*"/ + + %import common.WS + %ignore WS +""" + +try: + parser = Lark(test_grammar, start='start') + print("✓ Simple grammar loaded successfully!") + + # Test parsing + result = parser.parse('OUTPUT 42') + print(f"✓ Parsed: {result.pretty()}") + +except Exception as e: + print(f"✗ Error: {e}") diff --git a/frontend/src/components/compiler/examplePicker.tsx b/frontend/src/components/compiler/examplePicker.tsx index 32b473e..4b0ee11 100644 --- a/frontend/src/components/compiler/examplePicker.tsx +++ b/frontend/src/components/compiler/examplePicker.tsx @@ -8,37 +8,139 @@ interface Example { } const examples: Example[] = [ + // ========== Basics ========== { title: 'Hello World', category: 'Basics', code: `OUTPUT "Hello, World!"`, }, { - title: 'User Input', + title: 'Variables and Assignment', + category: 'Basics', + code: `DECLARE x : INTEGER +DECLARE y : INTEGER +x = 10 +y = 20 +OUTPUT "x = ", x +OUTPUT "y = ", y +OUTPUT "x + y = ", x + y`, + }, + { + title: 'Constants', + category: 'Basics', + code: `CONSTANT PI = 3.14159 +DECLARE radius : REAL +radius = 5 +area = PI * radius * radius +OUTPUT "Area of circle: ", area`, + }, + + // ========== Input/Output ========== + { + title: 'Simple Input', category: 'Input/Output', - code: `INPUT name + code: `DECLARE name : STRING +INPUT name OUTPUT "Hello, ", name`, }, { - title: 'Simple Loop', + title: 'Multiple Inputs', + category: 'Input/Output', + code: `DECLARE length : REAL +DECLARE width : REAL +OUTPUT "Enter length: " +INPUT length +OUTPUT "Enter width: " +INPUT width +area = length * width +OUTPUT "Area = ", area`, + }, + + // ========== Conditionals ========== + { + title: 'IF Statement', + category: 'Conditionals', + code: `DECLARE age : INTEGER +INPUT age +IF age >= 18 THEN + OUTPUT "You are an adult" +ELSE + OUTPUT "You are a minor" +ENDIF`, + }, + { + title: 'Nested IF', + category: 'Conditionals', + code: `DECLARE score : INTEGER +INPUT score +IF score >= 90 THEN + OUTPUT "Grade: A" +ELSEIF score >= 80 THEN + OUTPUT "Grade: B" +ELSEIF score >= 70 THEN + OUTPUT "Grade: C" +ELSEIF score >= 60 THEN + OUTPUT "Grade: D" +ELSE + OUTPUT "Grade: F" +ENDIF`, + }, + + // ========== Loops ========== + { + title: 'FOR Loop', + category: 'Loops', + code: `DECLARE i : INTEGER +FOR i = 1 TO 10 + OUTPUT i +NEXT i`, + }, + { + title: 'FOR Loop with STEP', category: 'Loops', - code: `FOR i = 1 TO 5 + code: `DECLARE i : INTEGER +FOR i = 0 TO 20 STEP 2 OUTPUT i NEXT i`, }, { - title: 'While Loop', + title: 'WHILE Loop', category: 'Loops', - code: `count = 1 + code: `DECLARE count : INTEGER +count = 1 WHILE count <= 5 DO OUTPUT count count = count + 1 ENDWHILE`, }, { - title: 'Array Operations', + title: 'REPEAT Loop', + category: 'Loops', + code: `DECLARE num : INTEGER +num = 1 +REPEAT + OUTPUT num + num = num + 1 +UNTIL num > 5`, + }, + { + title: 'Nested Loops', + category: 'Loops', + code: `DECLARE i : INTEGER +DECLARE j : INTEGER +FOR i = 1 TO 3 + FOR j = 1 TO 3 + OUTPUT i, " x ", j, " = ", i * j + NEXT j +NEXT i`, + }, + + // ========== Arrays ========== + { + title: '1D Array', category: 'Arrays', code: `DECLARE numbers : ARRAY[1:5] OF INTEGER +DECLARE i : INTEGER FOR i = 1 TO 5 numbers[i] = i * 2 NEXT i @@ -47,142 +149,240 @@ FOR i = 1 TO 5 NEXT i`, }, { - title: 'Calculate Average', - category: 'Math', - code: `DECLARE numbers : ARRAY[1:5] OF INTEGER -sum = 0 - -FOR i = 1 TO 5 - INPUT numbers[i] - sum = sum + numbers[i] + title: '2D Array', + category: 'Arrays', + code: `DECLARE matrix : ARRAY[1:3, 1:3] OF INTEGER +DECLARE i : INTEGER +DECLARE j : INTEGER +FOR i = 1 TO 3 + FOR j = 1 TO 3 + matrix[i, j] = i * j + NEXT j NEXT i - -average = sum / 5 -OUTPUT "The average is: ", average`, +FOR i = 1 TO 3 + FOR j = 1 TO 3 + OUTPUT matrix[i, j], " " + NEXT j + OUTPUT "" +NEXT i`, }, { - title: 'Find Maximum', + title: 'Find Maximum in Array', category: 'Arrays', code: `DECLARE numbers : ARRAY[1:5] OF INTEGER +DECLARE i : INTEGER +DECLARE max : INTEGER FOR i = 1 TO 5 INPUT numbers[i] NEXT i - max = numbers[1] FOR i = 2 TO 5 IF numbers[i] > max THEN max = numbers[i] ENDIF NEXT i +OUTPUT "Maximum: ", max`, + }, -OUTPUT "Maximum number is: ", max`, + // ========== Procedures ========== + { + title: 'Simple Procedure', + category: 'Procedures', + code: `PROCEDURE Greet() + OUTPUT "Hello from procedure!" +ENDPROCEDURE + +CALL Greet()`, }, { - title: 'Temperature Converter', - category: 'Math', - code: `INPUT celsius -fahrenheit = (celsius * 9/5) + 32 -OUTPUT celsius, "°C is ", fahrenheit, "°F"`, + title: 'Procedure with Parameters', + category: 'Procedures', + code: `PROCEDURE PrintSum(a : INTEGER, b : INTEGER) + DECLARE sum : INTEGER + sum = a + b + OUTPUT "Sum = ", sum +ENDPROCEDURE + +CALL PrintSum(5, 3) +CALL PrintSum(10, 20)`, }, { - title: 'Simple Calculator', - category: 'Math', - code: `INPUT num1 -INPUT op -INPUT num2 - -IF op = "+" THEN - result = num1 + num2 -ELSEIF op = "-" THEN - result = num1 - num2 -ELSEIF op = "*" THEN - result = num1 * num2 -ELSEIF op = "/" THEN - IF num2 = 0 THEN - OUTPUT "Error: Cannot divide by zero" - ELSE - result = num1 / num2 + title: 'Procedure with BYREF', + category: 'Procedures', + code: `PROCEDURE Swap(BYREF a : INTEGER, BYREF b : INTEGER) + DECLARE temp : INTEGER + temp = a + a = b + b = temp +ENDPROCEDURE + +DECLARE x : INTEGER +DECLARE y : INTEGER +x = 5 +y = 10 +OUTPUT "Before: x=", x, ", y=", y +CALL Swap(x, y) +OUTPUT "After: x=", x, ", y=", y`, + }, + + // ========== Functions ========== + { + title: 'Simple Function', + category: 'Functions', + code: `FUNCTION Square(n : INTEGER) RETURNS INTEGER + RETURN n * n +ENDFUNCTION + +DECLARE result : INTEGER +result = Square(5) +OUTPUT "5 squared = ", result`, + }, + { + title: 'Function with Multiple Parameters', + category: 'Functions', + code: `FUNCTION Add(a : INTEGER, b : INTEGER) RETURNS INTEGER + RETURN a + b +ENDFUNCTION + +DECLARE sum : INTEGER +sum = Add(10, 20) +OUTPUT "Sum = ", sum`, + }, + { + title: 'Factorial Function', + category: 'Functions', + code: `FUNCTION Factorial(n : INTEGER) RETURNS INTEGER + DECLARE result : INTEGER + DECLARE i : INTEGER + result = 1 + FOR i = 1 TO n + result = result * i + NEXT i + RETURN result +ENDFUNCTION + +DECLARE num : INTEGER +DECLARE fact : INTEGER +num = 5 +fact = Factorial(num) +OUTPUT num, "! = ", fact`, + }, + { + title: 'Is Prime Function', + category: 'Functions', + code: `FUNCTION IsPrime(n : INTEGER) RETURNS BOOLEAN + DECLARE i : INTEGER + IF n <= 1 THEN + RETURN FALSE ENDIF -ENDIF + FOR i = 2 TO n - 1 + IF n MOD i = 0 THEN + RETURN FALSE + ENDIF + NEXT i + RETURN TRUE +ENDFUNCTION -OUTPUT num1, " ", op, " ", num2, " = ", result`, +DECLARE num : INTEGER +num = 17 +IF IsPrime(num) THEN + OUTPUT num, " is prime" +ELSE + OUTPUT num, " is not prime" +ENDIF`, }, + + // ========== Strings ========== { - title: 'UserInput Example', - category: 'Input/Output', - code: `name = UserInput -age = UserInput -OUTPUT "Hello, ", name, "! You are ", age, " years old."`, + title: 'String Operations', + category: 'Strings', + code: `DECLARE text : STRING +text = "Hello" +OUTPUT "Length: ", LENGTH(text) +OUTPUT "Substring: ", SUBSTRING(text, 1, 3)`, }, { - title: 'Array with UserInput', - category: 'Arrays', - code: `DECLARE scores : ARRAY[1:5] OF INTEGER -sum = 0 + title: 'String Concatenation', + category: 'Strings', + code: `DECLARE first : STRING +DECLARE last : STRING +DECLARE full : STRING +first = "John" +last = "Doe" +full = first + " " + last +OUTPUT "Full name: ", full`, + }, + // ========== Complete Programs ========== + { + title: 'Calculate Average', + category: 'Complete Programs', + code: `DECLARE numbers : ARRAY[1:5] OF INTEGER +DECLARE i : INTEGER +DECLARE sum : INTEGER +DECLARE average : REAL +sum = 0 FOR i = 1 TO 5 - scores[i] = UserInput - sum = sum + scores[i] + OUTPUT "Enter number ", i, ": " + INPUT numbers[i] + sum = sum + numbers[i] NEXT i - average = sum / 5 -OUTPUT "The average score is: ", average`, +OUTPUT "Average = ", average`, }, { - title: 'Input Validation Loop', - category: 'Input/Output', - code: `valid = FALSE -WHILE NOT valid DO - password = UserInput - IF LENGTH(password) < 8 THEN - OUTPUT "Password too short, must be at least 8 characters" - ELSE - valid = TRUE - OUTPUT "Password accepted" + title: 'Number Guessing Game', + category: 'Complete Programs', + code: `DECLARE secret : INTEGER +DECLARE guess : INTEGER +DECLARE attempts : INTEGER +secret = 42 +attempts = 0 +OUTPUT "Guess the number (1-100)!" +REPEAT + INPUT guess + attempts = attempts + 1 + IF guess < secret THEN + OUTPUT "Too low!" + ELSEIF guess > secret THEN + OUTPUT "Too high!" ENDIF -ENDWHILE`, +UNTIL guess = secret +OUTPUT "Correct! You got it in ", attempts, " attempts"`, }, { - title: 'Interactive Menu', - category: 'Input/Output', - code: `choice = 0 -WHILE choice <> 4 DO - OUTPUT "===== MENU =====" - OUTPUT "1. Option One" - OUTPUT "2. Option Two" - OUTPUT "3. Option Three" - OUTPUT "4. Exit" - - choice = UserInput - - IF choice = 1 THEN - OUTPUT "You selected Option One" - ELSEIF choice = 2 THEN - OUTPUT "You selected Option Two" - ELSEIF choice = 3 THEN - OUTPUT "You selected Option Three" - ELSEIF choice <> 4 THEN - OUTPUT "Invalid choice, please try again" + title: 'Bubble Sort', + category: 'Complete Programs', + code: `DECLARE arr : ARRAY[1:5] OF INTEGER +DECLARE i : INTEGER +DECLARE j : INTEGER +DECLARE temp : INTEGER +DECLARE swapped : BOOLEAN + +OUTPUT "Enter 5 numbers:" +FOR i = 1 TO 5 + INPUT arr[i] +NEXT i + +FOR i = 1 TO 4 + swapped = FALSE + FOR j = 1 TO 5 - i + IF arr[j] > arr[j + 1] THEN + temp = arr[j] + arr[j] = arr[j + 1] + arr[j + 1] = temp + swapped = TRUE + ENDIF + NEXT j + IF NOT swapped THEN + i = 5 ENDIF -ENDWHILE`, - }, - { - title: 'Compound Interest Calculator', - category: 'Finance', - code: `OUTPUT "Enter principal amount: " -principal = UserInput -OUTPUT "Enter annual interest rate (as decimal): " -rate = UserInput -OUTPUT "Enter time period in years: " -time = UserInput -OUTPUT "Enter number of times interest is compounded per year: " -n = UserInput - -amount = principal * (1 + rate/n) ^ (n * time) -interest = amount - principal - -OUTPUT "Final amount: ", amount -OUTPUT "Interest earned: ", interest`, +NEXT i + +OUTPUT "Sorted array:" +FOR i = 1 TO 5 + OUTPUT arr[i] +NEXT i`, }, ];