@@ -101,3 +101,213 @@ impl Repl {
101101 }
102102 }
103103}
104+
105+ /// Returns true when the given `input` looks like a complete JavaScript
106+ /// top-level expression/program piece (i.e. brackets and template expressions
107+ /// are balanced, strings/comments/regex literals are properly closed).
108+ ///
109+ /// This uses heuristics (not a full parser) but covers common REPL cases:
110+ /// - ignores brackets inside single/double-quoted strings
111+ /// - supports template literals and nested ${ ... } expressions
112+ /// - ignores brackets inside // and /* */ comments
113+ /// - attempts to detect regex literals using a simple context heuristic and
114+ /// ignores brackets inside them
115+ pub fn is_complete_input ( src : & str ) -> bool {
116+ let mut bracket_stack: Vec < char > = Vec :: new ( ) ;
117+ let mut in_single = false ;
118+ let mut in_double = false ;
119+ let mut in_backtick = false ;
120+ let mut in_line_comment = false ;
121+ let mut in_block_comment = false ;
122+ let mut in_regex = false ;
123+ let mut escape = false ;
124+
125+ // small helper returns whether a char is considered a token that can
126+ // precede a regex literal (heuristic).
127+ fn can_start_regex ( prev : Option < char > ) -> bool {
128+ match prev {
129+ None => true ,
130+ Some ( p) => matches ! (
131+ p,
132+ '(' | ',' | '=' | ':' | '[' | '!' | '?' | '{' | '}' | ';' | '\n' | '\r' | '\t' | ' '
133+ ) ,
134+ }
135+ }
136+
137+ let mut prev_non_space: Option < char > = None ;
138+ let mut chars = src. chars ( ) . peekable ( ) ;
139+
140+ while let Some ( ch) = chars. next ( ) {
141+ // handle escaping inside strings/template/regex
142+ if escape {
143+ escape = false ;
144+ // don't treat escaped characters as structure
145+ continue ;
146+ }
147+
148+ // start of line comment
149+ if in_line_comment {
150+ if ch == '\n' || ch == '\r' {
151+ in_line_comment = false ;
152+ }
153+ prev_non_space = Some ( ch) ;
154+ continue ;
155+ }
156+
157+ // inside block comment
158+ if in_block_comment {
159+ if ch == '*'
160+ && let Some ( '/' ) = chars. peek ( ) . copied ( )
161+ {
162+ // consume '/'
163+ let _ = chars. next ( ) ;
164+ in_block_comment = false ;
165+ prev_non_space = Some ( '/' ) ;
166+ continue ;
167+ }
168+ prev_non_space = Some ( ch) ;
169+ continue ;
170+ }
171+
172+ // if inside a regex, look for unescaped trailing slash
173+ if in_regex {
174+ if ch == '\\' {
175+ escape = true ;
176+ continue ;
177+ }
178+ if ch == '/' {
179+ // consume optional flags after regex
180+ // we don't need to parse flags here, just stop regex mode
181+ in_regex = false ;
182+ // consume following letters (flags) without affecting structure
183+ while let Some ( & f) = chars. peek ( ) {
184+ if f. is_ascii_alphabetic ( ) {
185+ chars. next ( ) ;
186+ } else {
187+ break ;
188+ }
189+ }
190+ }
191+ prev_non_space = Some ( ch) ;
192+ continue ;
193+ }
194+
195+ // top-level string / template handling
196+ if in_single {
197+ if ch == '\\' {
198+ escape = true ;
199+ continue ;
200+ }
201+ if ch == '\'' {
202+ in_single = false ;
203+ }
204+ prev_non_space = Some ( ch) ;
205+ continue ;
206+ }
207+ if in_double {
208+ if ch == '\\' {
209+ escape = true ;
210+ continue ;
211+ }
212+ if ch == '"' {
213+ in_double = false ;
214+ }
215+ prev_non_space = Some ( ch) ;
216+ continue ;
217+ }
218+ if in_backtick {
219+ if ch == '\\' {
220+ escape = true ;
221+ continue ;
222+ }
223+ if ch == '`' {
224+ in_backtick = false ;
225+ prev_non_space = Some ( '`' ) ;
226+ continue ;
227+ }
228+ // template expression start: ${ ... }
229+ if ch == '$' && chars. peek ( ) == Some ( & '{' ) {
230+ // consume the '{'
231+ let _ = chars. next ( ) ;
232+ // treat it as an opening brace in the normal bracket stack
233+ bracket_stack. push ( '}' ) ;
234+ prev_non_space = Some ( '{' ) ;
235+ continue ;
236+ }
237+ // a closing '}' may appear while still inside the template literal
238+ // if it corresponds to a `${ ... }` expression — pop that marker
239+ if ch == '}' {
240+ if let Some ( expected) = bracket_stack. pop ( ) {
241+ if expected != '}' {
242+ return true ; // mismatched - treat as complete and surface parse error
243+ }
244+ } else {
245+ // unmatched closing brace inside template - treat as complete
246+ return true ;
247+ }
248+ prev_non_space = Some ( '}' ) ;
249+ continue ;
250+ }
251+ prev_non_space = Some ( ch) ;
252+ continue ;
253+ }
254+
255+ // not inside any obvious literal or comment
256+ match ch {
257+ '\'' => in_single = true ,
258+ '"' => in_double = true ,
259+ '`' => in_backtick = true ,
260+ '/' => {
261+ // Could be line comment '//' or block comment '/*' or regex literal '/.../'
262+ if let Some ( & next) = chars. peek ( ) {
263+ if next == '/' {
264+ // consume next and enter line comment
265+ let _ = chars. next ( ) ;
266+ in_line_comment = true ;
267+ prev_non_space = Some ( '/' ) ;
268+ continue ;
269+ } else if next == '*' {
270+ // consume next and enter block comment
271+ let _ = chars. next ( ) ;
272+ in_block_comment = true ;
273+ prev_non_space = Some ( '/' ) ;
274+ continue ;
275+ }
276+ }
277+
278+ // Heuristic: start regex when previous non-space allows it
279+ if can_start_regex ( prev_non_space) {
280+ in_regex = true ;
281+ prev_non_space = Some ( '/' ) ;
282+ continue ;
283+ }
284+
285+ // otherwise treat as division/operator and continue
286+ }
287+ '(' => bracket_stack. push ( ')' ) ,
288+ '[' => bracket_stack. push ( ']' ) ,
289+ '{' => bracket_stack. push ( '}' ) ,
290+ ')' | ']' | '}' => {
291+ if let Some ( expected) = bracket_stack. pop ( ) {
292+ if expected != ch {
293+ // mismatched closing; we treat the input as "complete"
294+ // so a syntax error will be surfaced by the evaluator.
295+ return true ;
296+ }
297+ } else {
298+ // extra closing bracket — still treat as complete so user gets a parse error
299+ return true ;
300+ }
301+ }
302+ _ => { }
303+ }
304+
305+ if !ch. is_whitespace ( ) {
306+ prev_non_space = Some ( ch) ;
307+ }
308+ }
309+
310+ // Input is complete if we are not inside any multiline construct and there are
311+ // no unmatched opening brackets remaining.
312+ !in_single && !in_double && !in_backtick && !in_block_comment && !in_regex && bracket_stack. is_empty ( )
313+ }
0 commit comments