Skip to content

Commit bcd9faf

Browse files
committed
logging: reduce noise for thrown values by logging JSError::Throw at debug level instead of error
1 parent aab1776 commit bcd9faf

File tree

5 files changed

+276
-270
lines changed

5 files changed

+276
-270
lines changed

src/core/eval.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,23 @@ fn evaluate_statements_with_context(env: &JSObjectDataPtr, statements: &[Stateme
891891
Ok(Some(cf)) => return Ok(cf),
892892
Ok(None) => {}
893893
Err(e) => {
894-
log::error!("evaluate_statements_with_context error at statement {}: {:?} stmt={:?}", i, e, stmt);
894+
// Thrown values (user code `throw`) are expected control flow and
895+
// are noisy when logged at error level during batch testing. Lower
896+
// their logging to debug so test runs aren't flooded. Keep other
897+
// engine/internal errors at error level.
898+
match &e {
899+
JSError::Throw { .. } => {
900+
log::debug!(
901+
"evaluate_statements_with_context thrown value at statement {}: {:?} stmt={:?}",
902+
i,
903+
e,
904+
stmt
905+
);
906+
}
907+
_ => {
908+
log::error!("evaluate_statements_with_context error at statement {}: {:?} stmt={:?}", i, e, stmt);
909+
}
910+
}
895911
return Err(e);
896912
}
897913
}

src/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ pub(crate) mod js_std;
2323
pub(crate) mod js_string;
2424
pub(crate) mod js_testintl;
2525
pub(crate) mod repl;
26-
pub(crate) mod repl_utils;
2726
pub(crate) mod sprintf;
2827
pub(crate) mod tmpfile;
2928
pub(crate) mod utf16;
@@ -39,5 +38,4 @@ pub use core::{
3938
JS_TAG_SHORT_BIG_INT, JS_TAG_STRING, JS_TAG_STRING_ROPE, JS_TAG_UNDEFINED, JS_UNINITIALIZED,
4039
};
4140
pub use error::JSError;
42-
pub use repl::Repl;
43-
pub use repl_utils::is_complete_input;
41+
pub use repl::{Repl, is_complete_input};

src/repl.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)