Skip to content

Commit 8c21607

Browse files
committed
Add bytecode IP-to-line/column mapping for accurate error reporting
- Add line_map field to Chunk storing (ip, line, column) tuples - Add record_line() and get_line_col_for_ip() methods to Chunk - Record statement line/column in compiler at compile_statement entry - Add last_throw_ip and current_opcode_ip fields to VM - Capture throw-site IP in run_opcode_throw and handle_throw - Replace text-search heuristics with direct IP-to-line/col lookup - Remove source_lines, find_function_declaration_line, find_line_and_column, infer_callsite - Simplify infer_throw_site and infer_callsite_from_call_ip to pure table lookups - Merge line_map entries in merge_eval_chunk for eval support - Clean up orphaned try frames on function return in run_opcode_return
1 parent 96bc651 commit 8c21607

3 files changed

Lines changed: 56 additions & 113 deletions

File tree

src/core/compiler.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ impl<'gc> Compiler<'gc> {
344344
}
345345

346346
fn compile_statement(&mut self, stmt: &Statement, is_last: bool) -> Result<(), JSError> {
347+
self.chunk.record_line(stmt.line, stmt.column);
347348
match &*stmt.kind {
348349
StatementKind::Expr(expr) => {
349350
// Detect 'use strict' directive at top-level or inside function

src/core/opcode.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ pub struct Chunk<'gc> {
207207
pub generator_function_ips: std::collections::HashSet<usize>,
208208
/// Function IPs that are method/getter/setter definitions and should not be constructible.
209209
pub method_function_ips: std::collections::HashSet<usize>,
210+
/// Bytecode offset → source (line, column) mapping (sorted by offset).
211+
pub line_map: Vec<(usize, usize, usize)>,
210212
}
211213

212214
impl<'gc> Chunk<'gc> {
@@ -224,6 +226,7 @@ impl<'gc> Chunk<'gc> {
224226
call_callee_names: std::collections::HashMap::new(),
225227
generator_function_ips: std::collections::HashSet::new(),
226228
method_function_ips: std::collections::HashSet::new(),
229+
line_map: Vec::new(),
227230
}
228231
}
229232

@@ -244,4 +247,24 @@ impl<'gc> Chunk<'gc> {
244247
self.constants.push(value);
245248
(self.constants.len() - 1) as u16
246249
}
250+
251+
/// Record a source line and column for the current bytecode offset.
252+
pub fn record_line(&mut self, line: usize, column: usize) {
253+
if line == 0 {
254+
return;
255+
}
256+
let ip = self.code.len();
257+
if self.line_map.last().map(|&(_, l, c)| (l, c)) != Some((line, column)) {
258+
self.line_map.push((ip, line, column));
259+
}
260+
}
261+
262+
/// Look up the source line and column for a bytecode IP.
263+
pub fn get_line_col_for_ip(&self, ip: usize) -> Option<(usize, usize)> {
264+
match self.line_map.binary_search_by_key(&ip, |&(offset, _, _)| offset) {
265+
Ok(idx) => Some((self.line_map[idx].1, self.line_map[idx].2)),
266+
Err(0) => None,
267+
Err(idx) => Some((self.line_map[idx - 1].1, self.line_map[idx - 1].2)),
268+
}
269+
}
247270
}

src/core/vm.rs

Lines changed: 32 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,10 @@ pub struct VM<'gc> {
534534
generator_yield_value: Option<Value<'gc>>,
535535
// Set by Opcode::GeneratorParamInitDone during generator call-time preflight.
536536
generator_param_init_done: bool,
537+
// Bytecode IP at which the most recent Throw opcode executed (for line-number reporting).
538+
last_throw_ip: Option<usize>,
539+
// IP of the opcode currently being executed.
540+
current_opcode_ip: usize,
537541
// %GeneratorPrototype% intrinsic — shared prototype for generator .prototype objects
538542
generator_prototype: Value<'gc>,
539543
// %GeneratorFunction.prototype% — proto for generator functions themselves
@@ -657,6 +661,8 @@ impl<'gc> VM<'gc> {
657661
next_generator_id: 1,
658662
generator_yield_value: None,
659663
generator_param_init_done: false,
664+
last_throw_ip: None,
665+
current_opcode_ip: 0,
660666
generator_prototype: Value::Undefined,
661667
generator_function_prototype: Value::Undefined,
662668
async_generator_prototype: Value::Undefined,
@@ -9125,6 +9131,9 @@ impl<'gc> VM<'gc> {
91259131
for ip in &eval_chunk.method_function_ips {
91269132
self.chunk.method_function_ips.insert(ip + code_offset);
91279133
}
9134+
for &(ip, line, col) in &eval_chunk.line_map {
9135+
self.chunk.line_map.push((ip + code_offset, line, col));
9136+
}
91289137

91299138
(code_offset, const_offset)
91309139
}
@@ -11424,10 +11433,6 @@ impl<'gc> VM<'gc> {
1142411433
self.script_path.as_deref().unwrap_or("<anonymous>")
1142511434
}
1142611435

11427-
fn source_lines(&self) -> Option<Vec<&str>> {
11428-
self.script_source.as_ref().map(|source| source.split('\n').collect())
11429-
}
11430-
1143111436
fn current_named_frames(&self) -> Vec<(usize, String)> {
1143211437
self.frames
1143311438
.iter()
@@ -11439,123 +11444,31 @@ impl<'gc> VM<'gc> {
1143911444
.collect()
1144011445
}
1144111446

11442-
fn find_function_declaration_line(lines: &[&str], function_name: &str) -> Option<usize> {
11443-
let patterns = [
11444-
format!("function {}(", function_name),
11445-
format!(".{} = function", function_name),
11446-
format!("{} = function", function_name),
11447-
format!("{}: function", function_name),
11448-
];
11449-
11450-
lines
11451-
.iter()
11452-
.enumerate()
11453-
.find(|(_, line)| patterns.iter().any(|pattern| line.contains(pattern)))
11454-
.map(|(index, _)| index + 1)
11455-
}
11456-
11457-
fn find_line_and_column(lines: &[&str], needle: &str, start_line: usize, end_line: usize) -> Option<(usize, usize)> {
11458-
let start_index = start_line.saturating_sub(1);
11459-
let end_index = end_line.min(lines.len());
11460-
for (offset, line) in lines[start_index..end_index].iter().enumerate() {
11461-
if let Some(column) = line.find(needle) {
11462-
return Some((start_index + offset + 1, column + 1));
11463-
}
11464-
}
11465-
None
11466-
}
11467-
11468-
fn infer_throw_site(&self, current_function: Option<&str>) -> Option<(usize, usize)> {
11469-
let lines = self.source_lines()?;
11470-
if let Some(function_name) = current_function
11471-
&& let Some(decl_line) = Self::find_function_declaration_line(&lines, function_name)
11472-
&& let Some(found) = Self::find_line_and_column(&lines, "throw ", decl_line, lines.len())
11473-
{
11474-
return Some(found);
11475-
}
11476-
Self::find_line_and_column(&lines, "throw ", 1, lines.len())
11477-
}
11478-
11479-
fn infer_callsite(&self, function_name: &str, scope_function: Option<&str>) -> Option<(usize, usize)> {
11480-
let lines = self.source_lines()?;
11481-
let call_patterns = [format!("{}(", function_name), format!(".{}(", function_name)];
11482-
11483-
let (start_line, end_line) = if let Some(scope_name) = scope_function {
11484-
if let Some(decl_line) = Self::find_function_declaration_line(&lines, scope_name) {
11485-
(decl_line, lines.len())
11486-
} else {
11487-
(1, lines.len())
11488-
}
11489-
} else {
11490-
(1, lines.len())
11491-
};
11492-
11493-
for line_number in start_line..=end_line {
11494-
let line = lines.get(line_number.saturating_sub(1))?;
11495-
if line.contains(&format!("function {}(", function_name)) {
11496-
continue;
11497-
}
11498-
if let Some(column) = line.find(&call_patterns[1]) {
11499-
return Some((line_number, column + 2));
11500-
}
11501-
if let Some(column) = line.find(&call_patterns[0]) {
11502-
return Some((line_number, column + 1));
11503-
}
11504-
}
11505-
11506-
None
11447+
/// Find the source line/column for the most recent throw, using the IP→line/col map.
11448+
fn infer_throw_site(&self) -> Option<(usize, usize)> {
11449+
let throw_ip = self.last_throw_ip?;
11450+
self.chunk.get_line_col_for_ip(throw_ip)
1150711451
}
1150811452

11453+
/// Find the source line/column for a call-site IP, using the IP→line/col map.
1150911454
fn infer_callsite_from_call_ip(&self, call_ip: usize) -> Option<(usize, usize)> {
11510-
let callee_name = self.chunk.call_callee_names.get(&call_ip)?;
11511-
let mut ips_for_name: Vec<usize> = self
11512-
.chunk
11513-
.call_callee_names
11514-
.iter()
11515-
.filter_map(|(ip, name)| if name == callee_name { Some(*ip) } else { None })
11516-
.collect();
11517-
ips_for_name.sort_unstable();
11518-
let ordinal = ips_for_name.iter().position(|ip| *ip == call_ip)?;
11519-
11520-
let lines = self.source_lines()?;
11521-
let call_patterns = [format!("{}(", callee_name), format!(".{}(", callee_name)];
11522-
let mut seen = 0usize;
11523-
for (idx, line) in lines.iter().enumerate() {
11524-
if line.contains(&format!("function {}(", callee_name)) {
11525-
continue;
11526-
}
11527-
if let Some(column) = line.find(&call_patterns[1]) {
11528-
if seen == ordinal {
11529-
return Some((idx + 1, column + 2));
11530-
}
11531-
seen += 1;
11532-
continue;
11533-
}
11534-
if let Some(column) = line.find(&call_patterns[0]) {
11535-
if seen == ordinal {
11536-
return Some((idx + 1, column + 1));
11537-
}
11538-
seen += 1;
11539-
}
11540-
}
11541-
11542-
None
11455+
self.chunk.get_line_col_for_ip(call_ip)
1154311456
}
1154411457

1154511458
fn build_error_stack(&self, error_name: &str, message: &str) -> (Option<(usize, usize)>, Vec<String>) {
1154611459
let mut lines = vec![Self::format_error_name_message(error_name, message)];
1154711460
let named_frames = self.current_named_frames();
1154811461

1154911462
if named_frames.is_empty() {
11550-
if let Some((line, column)) = self.infer_throw_site(None) {
11463+
if let Some((line, column)) = self.infer_throw_site() {
1155111464
lines.push(format!(" at <anonymous> ({}:{}:{})", self.current_script_file(), line, column));
1155211465
return (Some((line, column)), lines);
1155311466
}
1155411467
return (None, lines);
1155511468
}
1155611469

1155711470
let current_function = named_frames.last().map(|(_, name)| name.as_str());
11558-
let throw_site = self.infer_throw_site(current_function);
11471+
let throw_site = self.infer_throw_site();
1155911472
if let Some((line, column)) = throw_site {
1156011473
let function_name = current_function.unwrap_or("<anonymous>");
1156111474
lines.push(format!(
@@ -11569,12 +11482,9 @@ impl<'gc> VM<'gc> {
1156911482

1157011483
for pair in named_frames.windows(2).rev() {
1157111484
let caller_name = &pair[0].1;
11572-
let callee_name = &pair[1].1;
1157311485
let callee_frame_idx = pair[1].0;
1157411486
let call_ip = self.frames[callee_frame_idx].return_ip.saturating_sub(2);
11575-
let call_site = self
11576-
.infer_callsite_from_call_ip(call_ip)
11577-
.or_else(|| self.infer_callsite(callee_name, Some(caller_name)));
11487+
let call_site = self.infer_callsite_from_call_ip(call_ip);
1157811488
if let Some((line, column)) = call_site {
1157911489
lines.push(format!(
1158011490
" at {} ({}:{}:{})",
@@ -11586,12 +11496,9 @@ impl<'gc> VM<'gc> {
1158611496
}
1158711497
}
1158811498

11589-
if let Some((outer_idx, outermost_name)) = named_frames.first() {
11499+
if let Some((outer_idx, _outermost_name)) = named_frames.first() {
1159011500
let outer_call_ip = self.frames[*outer_idx].return_ip.saturating_sub(2);
11591-
if let Some((line, column)) = self
11592-
.infer_callsite_from_call_ip(outer_call_ip)
11593-
.or_else(|| self.infer_callsite(outermost_name, None))
11594-
{
11501+
if let Some((line, column)) = self.infer_callsite_from_call_ip(outer_call_ip) {
1159511502
lines.push(format!(" at <anonymous> ({}:{}:{})", self.current_script_file(), line, column));
1159611503
}
1159711504
}
@@ -24405,6 +24312,10 @@ impl<'gc> VM<'gc> {
2440524312
/// Handle a thrown value: unwind to nearest try/catch or return error
2440624313
fn handle_throw(&mut self, ctx: &GcContext<'gc>, thrown: &Value<'gc>) -> Result<(), JSError> {
2440724314
self.pending_throw = None;
24315+
// Record the throw-site IP if not already set by a Throw opcode.
24316+
if self.last_throw_ip.is_none() {
24317+
self.last_throw_ip = Some(self.current_opcode_ip);
24318+
}
2440824319
if let Value::VmObject(map) = &thrown {
2440924320
self.annotate_error_object(ctx, map);
2441024321
}
@@ -25021,6 +24932,7 @@ impl<'gc> VM<'gc> {
2502124932
continue;
2502224933
}
2502324934
// Fetch instruction
24935+
self.current_opcode_ip = self.ip;
2502424936
let instruction_byte = self.read_byte();
2502524937
let instruction = Opcode::try_from(instruction_byte)?;
2502624938

@@ -25141,6 +25053,11 @@ impl<'gc> VM<'gc> {
2514125053
if frame.is_method {
2514225054
self.this_stack.pop();
2514325055
}
25056+
// Pop any try frames that belonged to the returning function.
25057+
let current_depth = self.frames.len();
25058+
while self.try_stack.last().is_some_and(|tf| tf.frame_depth > current_depth) {
25059+
self.try_stack.pop();
25060+
}
2514425061
self.stack.truncate(frame.bp - 1);
2514525062
self.ip = frame.return_ip;
2514625063
if self.frames.len() < min_depth {
@@ -28377,6 +28294,8 @@ impl<'gc> VM<'gc> {
2837728294
// Opcode::Throw
2837828295
fn run_opcode_throw(&mut self, ctx: &GcContext<'gc>) -> Result<OpcodeAction<'gc>, JSError> {
2837928296
let thrown = self.stack.pop().unwrap_or(Value::Undefined);
28297+
// Record the throw-site IP for accurate line-number reporting.
28298+
self.last_throw_ip = Some(self.current_opcode_ip);
2838028299
// diagnostic logging
2838128300
log::warn!("Throw opcode value={}", self.vm_to_string(ctx, &thrown));
2838228301
if let Value::VmObject(obj) = &thrown {

0 commit comments

Comments
 (0)