1- import { useState , useEffect , useRef } from "react" ;
2- import { loadPyodide , type PyodideInterface } from "pyodide" ;
1+ import { useState } from "react" ;
2+
3+ // ---- types shared between executor and props ----
4+ export type ExpectedStep =
5+ | { kind : "assign" ; var : string ; val : number }
6+ | { kind : "print" ; val : number } ;
37
48interface CodeboxProps {
59 lightcol : string ;
610 darkcol : string ;
711 headerimage : string ;
8- onOutput : ( output : string ) => void ;
12+ onOutput : ( output : string , isError : boolean ) => void ;
13+ expectedTrace ?: ExpectedStep [ ] ;
914}
1015
11- export default function Codebox ( props : CodeboxProps ) {
12-
13- const [ code , setCode ] = useState ( "" ) ;
14-
15- ////////
16- const [ isRunning , setIsRunning ] = useState ( false ) ;
17- const [ pyodideReady , setPyodideReady ] = useState ( false ) ;
18-
19- // Store the Pyodide instance so we only load it once
20- const pyodideRef = useRef < PyodideInterface | null > ( null ) ;
21-
22- // ←←← LOAD PYODIDE ONLY ONCE ←←←
23- useEffect ( ( ) => {
24- let mounted = true ;
16+ // ---- expression validator ----
17+ // Allowed: single-char vars x/y/z, literal 1 only, operators +-*^, parens.
18+ // Spaces are ignored. :) and :( must be together (enforced by caller regex).
19+ function validateExpression ( expr : string ) : boolean {
20+ const s = expr . replace ( / \s / g, "" ) ;
21+ if ( s . length === 0 ) return false ;
22+ if ( s . includes ( "**" ) ) return false ;
23+
24+ let i = 0 ;
25+ while ( i < s . length ) {
26+ const ch = s [ i ] ;
27+ if ( "xyz" . includes ( ch ) ) {
28+ // no multi-char identifiers
29+ if ( i + 1 < s . length && / [ a - z A - Z ] / . test ( s [ i + 1 ] ) ) return false ;
30+ i ++ ;
31+ } else if ( ch === "1" ) {
32+ // only 1 is a valid literal — 11, 12, etc. are invalid
33+ if ( i + 1 < s . length && / [ 0 - 9 ] / . test ( s [ i + 1 ] ) ) return false ;
34+ i ++ ;
35+ } else if ( / [ 0 - 9 ] / . test ( ch ) ) {
36+ return false ; // 2, 3, … all forbidden
37+ } else if ( "+-*^()" . includes ( ch ) ) {
38+ i ++ ;
39+ } else {
40+ return false ;
41+ }
42+ }
43+ return true ;
44+ }
2545
26- const initialize = async ( ) => {
27- try {
28- const pyodide = await loadPyodide ( {
29- indexURL : "https://cdn.jsdelivr.net/pyodide/v0.29.3/full/" ,
30- } ) ;
46+ function approxEqual ( a : number , b : number ) : boolean {
47+ return Math . abs ( a - b ) < 1e-9 ;
48+ }
3149
32- if ( mounted ) {
33- pyodideRef . current = pyodide ;
34- setPyodideReady ( true ) ;
35- }
36- } catch ( err ) {
37- console . error ( "Failed to load Pyodide" , err ) ;
38- props . onOutput ?.( "Failed to load Python runtime 😢" ) ;
50+ // ---- interpreter ----
51+ type AlienStep =
52+ | { kind : "assign" ; var : string ; val : number ; lineNum : number }
53+ | { kind : "print" ; val : number ; lineNum : number } ;
54+
55+ function runAlienCode ( code : string , expectedTrace ?: ExpectedStep [ ] ) : { output : string ; error : boolean } {
56+ // Trim every line, drop blanks, keep original line numbers
57+ const lines : Array < { text : string ; lineNum : number } > = code
58+ . split ( "\n" )
59+ . map ( ( text , i ) => ( { text : text . trim ( ) , lineNum : i + 1 } ) )
60+ . filter ( ( l ) => l . text . length > 0 ) ;
61+
62+ if ( lines . length === 0 ) {
63+ return { output : "Alien Error [Line 1]: Syntax Error" , error : true } ;
64+ }
65+
66+ // :) assignment — space allowed around :) but NOT inside it
67+ const ASSIGNMENT_RE = / ^ ( [ x y z ] ) \s * : \) \s * ( .+ ) $ / ;
68+ // :( print — any of x/y/z, space allowed before :(, NOT inside it
69+ const PRINT_RE = / ^ ( [ x y z ] ) \s * : \( $ / ;
70+
71+ const vars : Record < string , number > = { } ;
72+ const alienTrace : AlienStep [ ] = [ ] ;
73+ const outputLines : string [ ] = [ ] ;
74+
75+ for ( const { text : line , lineNum } of lines ) {
76+ // --- print ---
77+ const printMatch = line . match ( PRINT_RE ) ;
78+ if ( printMatch ) {
79+ const v = printMatch [ 1 ] ;
80+ if ( vars [ v ] === undefined ) {
81+ return { output : `Alien Error [Line ${ lineNum } ]: Syntax Error` , error : true } ;
82+ }
83+ const val = vars [ v ] ;
84+ outputLines . push ( String ( val ) ) ;
85+ alienTrace . push ( { kind : "print" , val, lineNum } ) ;
86+ continue ;
87+ }
88+
89+ // --- assignment ---
90+ const assignMatch = line . match ( ASSIGNMENT_RE ) ;
91+ if ( assignMatch ) {
92+ const varName = assignMatch [ 1 ] ;
93+ const expr = assignMatch [ 2 ] . trim ( ) ;
94+
95+ if ( ! validateExpression ( expr ) ) {
96+ return { output : `Alien Error [Line ${ lineNum } ]: Syntax Error` , error : true } ;
97+ }
98+
99+ // every referenced variable must be defined
100+ const exprClean = expr . replace ( / \s / g, "" ) ;
101+ for ( const v of [ "x" , "y" , "z" ] ) {
102+ if ( exprClean . includes ( v ) && vars [ v ] === undefined ) {
103+ return { output : `Alien Error [Line ${ lineNum } ]: Syntax Error` , error : true } ;
39104 }
40- } ;
41-
42- initialize ( ) ;
43-
44- return ( ) => {
45- mounted = false ;
46- } ;
47- } , [ props . onOutput ] ) ;
48-
49- const runCode = async ( ) => {
50- const pyodide = pyodideRef . current ;
51- if ( ! pyodide ) {
52- props . onOutput ?.( "Python runtime is still loading..." ) ;
53- return ;
105+ }
106+
107+ try {
108+ const jsExpr = expr . replace ( / \^ / g, "**" ) ;
109+ const result = new Function ( "x" , "y" , "z" , `"use strict"; return (${ jsExpr } )` ) (
110+ vars [ "x" ] , vars [ "y" ] , vars [ "z" ]
111+ ) ;
112+ if ( typeof result !== "number" || ! isFinite ( result ) ) {
113+ return { output : `Alien Error [Line ${ lineNum } ]: Syntax Error` , error : true } ;
54114 }
115+ vars [ varName ] = result ;
116+ alienTrace . push ( { kind : "assign" , var : varName , val : result , lineNum } ) ;
117+ } catch {
118+ return { output : `Alien Error [Line ${ lineNum } ]: Syntax Error` , error : true } ;
119+ }
120+ continue ;
121+ }
122+
123+ // --- unrecognised line ---
124+ return { output : `Alien Error [Line ${ lineNum } ]: Syntax Error` , error : true } ;
125+ }
126+
127+ if ( outputLines . length === 0 ) {
128+ return { output : "No output" , error : true } ;
129+ }
130+
131+ // ---- line-by-line translation check ----
132+ if ( expectedTrace && expectedTrace . length > 0 ) {
133+ if ( alienTrace . length !== expectedTrace . length ) {
134+ return { output : "Alien Error: Translation incorrect" , error : true } ;
135+ }
136+
137+ for ( let i = 0 ; i < alienTrace . length ; i ++ ) {
138+ const a = alienTrace [ i ] ;
139+ const e = expectedTrace [ i ] ;
140+
141+ if ( a . kind !== e . kind ) {
142+ return { output : `Alien Error [Line ${ a . lineNum } ]: Translation incorrect` , error : true } ;
143+ }
144+
145+ if ( a . kind === "assign" && e . kind === "assign" ) {
146+ if ( a . var !== e . var || ! approxEqual ( a . val , e . val ) ) {
147+ return { output : `Alien Error [Line ${ a . lineNum } ]: Translation incorrect` , error : true } ;
148+ }
149+ }
55150
56- setIsRunning ( true ) ;
57- let output = "" ;
58-
59- try {
60- // Capture print() output
61- pyodide . setStdout ( {
62- batched : ( msg : string ) => {
63- output += msg + "\n" ;
64- } ,
65- } ) ;
66-
67- const result = await pyodide . runPythonAsync ( code ) ;
68-
69- const fullOutput =
70- output . trim ( ) +
71- ( result !== undefined ? "\n" + String ( result ) : "" ) ;
72-
73- props . onOutput ?.( fullOutput || "Code executed successfully (no output)" ) ;
74- } catch ( err : any ) {
75- props . onOutput ?.( `Error: ${ err . message } ` ) ;
76- console . error ( err ) ;
77- } finally {
78- setIsRunning ( false ) ;
151+ if ( a . kind === "print" && e . kind === "print" ) {
152+ if ( ! approxEqual ( a . val , e . val ) ) {
153+ return { output : `Alien Error [Line ${ a . lineNum } ]: Translation incorrect` , error : true } ;
79154 }
80- } ;
81- ////////
82-
83- return (
84- < div style = { { backgroundColor : props . lightcol , boxShadow : `0 12px 0 ${ props . darkcol } ` } } className = { `rounded-2xl overflow-hidden flex flex-col mt-25` } >
85- < div className = "flex items-center justify-center p-4" >
86- < img src = { "/" + props . headerimage } alt = { props . headerimage } className = "relative h-12 w-auto object-contain" />
87- </ div >
88-
89- < textarea style = { { fontFamily : "'Fira Code', monospace" , color : "white" , backgroundColor : props . darkcol } } value = { code } onChange = { ( e ) => setCode ( e . target . value ) } placeholder = "Write code here!"
90- className = "p-6 text-2xl mx-4 rounded-lg resize-none min-h-[300px]" > </ textarea >
91-
92- < div className = "m-4 mx-4 h-14" >
93- < button
94- onClick = { runCode }
95- disabled = { ! pyodideReady || isRunning }
96- style = { { backgroundColor : "#00A93D" , boxShadow : `0 6px 0 #00531F` , opacity : ! pyodideReady || isRunning ? 0.7 : 1 } }
97- className = "rounded-2xl hover:brightness-110 active:brightness-90"
98- >
99- < img src = "/run.png" alt = "" className = "h-14 px-6 py-3" />
100- </ button >
101- </ div >
102- </ div >
103- )
104- }
155+ }
156+ }
157+ }
158+
159+ return { output : outputLines . join ( "\n" ) , error : false } ;
160+ }
161+
162+ // ---- component ----
163+ export default function Codebox ( props : CodeboxProps ) {
164+ const [ code , setCode ] = useState ( "" ) ;
165+ const [ running , setRunning ] = useState ( false ) ;
166+
167+ const handleRun = ( ) => {
168+ setRunning ( true ) ;
169+ setTimeout ( ( ) => {
170+ const { output, error } = runAlienCode ( code , props . expectedTrace ) ;
171+ props . onOutput ( output , error ) ;
172+ setRunning ( false ) ;
173+ } , 300 ) ;
174+ } ;
175+
176+ return (
177+ < div
178+ style = { { backgroundColor : props . lightcol , boxShadow : `0 12px 0 ${ props . darkcol } ` } }
179+ className = "rounded-2xl overflow-hidden flex flex-col mt-25"
180+ >
181+ < div className = "flex items-center justify-center p-4" >
182+ < img src = { "/" + props . headerimage } alt = { props . headerimage } className = "relative h-12 w-auto object-contain" />
183+ </ div >
184+
185+ < textarea
186+ style = { { fontFamily : "'Fira Code', monospace" , color : "white" , backgroundColor : props . darkcol } }
187+ value = { code }
188+ onChange = { ( e ) => setCode ( e . target . value ) }
189+ placeholder = "Write alien code here!"
190+ className = "p-6 text-2xl mx-4 rounded-lg resize-none min-h-[300px]"
191+ />
192+
193+ < div className = "m-4 mx-4 h-14" >
194+ < button
195+ onClick = { handleRun }
196+ disabled = { running }
197+ style = { {
198+ backgroundColor : "#00A93D" ,
199+ boxShadow : "0 6px 0 #00531F" ,
200+ opacity : running ? 0.7 : 1 ,
201+ cursor : running ? "not-allowed" : "pointer" ,
202+ } }
203+ className = "rounded-2xl hover:brightness-110 active:brightness-90"
204+ >
205+ < img src = "/run.png" alt = "" className = "h-14 px-6 py-3" />
206+ </ button >
207+ </ div >
208+ </ div >
209+ ) ;
210+ }
0 commit comments