From f971752d7d4446cfde3fbafee68d89c43d14b465 Mon Sep 17 00:00:00 2001 From: lauren Date: Tue, 19 May 2026 14:10:33 -0700 Subject: [PATCH 1/2] [rust-compiler] Add fixture documenting TSAsExpression assignment-target failure A TS cast used as an assignment target, e.g. `(obj.x as number) = 1`, is a valid Babel LVal. The Rust port currently hard-fails at AST JSON deserialization because PatternLike has no variant for the TS cast wrappers. This fixture snapshots that broken behavior: the baseline records the unexpected `Failed to parse AST JSON: unknown variant TSAsExpression` error. The TS reference instead emits a graceful FindContextIdentifiers Todo; the next commit makes Rust match it. --- ...-as-expression-assignment-target.expect.md | 24 +++++++++++++++++++ ...o-rust-as-expression-assignment-target.tsx | 9 +++++++ 2 files changed, 33 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.expect.md new file mode 100644 index 00000000000..de5e29e0f39 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.expect.md @@ -0,0 +1,24 @@ + +## Input + +```javascript +function Component(props: {obj: {x: unknown}}) { + (props.obj.x as unknown as number) = 1; + return
{props.obj.x as number}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{obj: {x: 0}}], +}; + +``` + + +## Error + +``` +Failed to parse AST JSON: unknown variant `TSAsExpression`, expected one of `Identifier`, `ObjectPattern`, `ArrayPattern`, `AssignmentPattern`, `RestElement`, `MemberExpression` at line 1 column 9800 +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.tsx new file mode 100644 index 00000000000..52a96e305ee --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.tsx @@ -0,0 +1,9 @@ +function Component(props: {obj: {x: unknown}}) { + (props.obj.x as unknown as number) = 1; + return
{props.obj.x as number}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{obj: {x: 0}}], +}; From 4578ef7a6704a370f44af4e4a91e3400cda179b1 Mon Sep 17 00:00:00 2001 From: lauren Date: Tue, 19 May 2026 14:11:26 -0700 Subject: [PATCH 2/2] [rust-compiler] Handle TS cast wrappers as assignment targets Babel's LVal includes TSAsExpression, TSSatisfiesExpression, TSNonNullExpression and TSTypeAssertion (e.g. `(x as T) = ...`). PatternLike had no variants for these, so the NAPI AST JSON deserializer hard-failed before compilation. Add the four variants to PatternLike, mirroring the existing MemberExpression-as-LVal precedent. find_context_identifiers now records the same graceful FindContextIdentifiers Todo the TS reference emits for these targets; all other PatternLike matches get minimal non-recording exhaustive arms (the visitor still recurses into the inner expression). The fixture baseline flips from the unexpected serde failure to the Todo bailout, and now passes under both `yarn snap` and `yarn snap --rust`. --- .../react_compiler/src/entrypoint/program.rs | 13 ++- .../entrypoint/validate_source_locations.rs | 8 ++ .../crates/react_compiler_ast/src/patterns.rs | 4 + .../crates/react_compiler_ast/src/visitor.rs | 8 ++ .../react_compiler_lowering/src/build_hir.rs | 26 ++++- .../src/find_context_identifiers.rs | 104 ++++++++++++++++-- .../react_compiler_oxc/src/convert_ast.rs | 4 + .../src/convert_ast_reverse.rs | 16 ++- .../src/convert_ast_reverse.rs | 20 ++++ ...-as-expression-assignment-target.expect.md | 12 +- 10 files changed, 200 insertions(+), 15 deletions(-) diff --git a/compiler/crates/react_compiler/src/entrypoint/program.rs b/compiler/crates/react_compiler/src/entrypoint/program.rs index bc1585df595..66fe552dca7 100644 --- a/compiler/crates/react_compiler/src/entrypoint/program.rs +++ b/compiler/crates/react_compiler/src/entrypoint/program.rs @@ -899,7 +899,12 @@ fn calls_hooks_or_creates_jsx_in_pattern(pattern: &PatternLike) -> bool { .map_or(false, |e| calls_hooks_or_creates_jsx_in_pattern(e)) }), PatternLike::RestElement(rest) => calls_hooks_or_creates_jsx_in_pattern(&rest.argument), - PatternLike::Identifier(_) | PatternLike::MemberExpression(_) => false, + PatternLike::Identifier(_) + | PatternLike::MemberExpression(_) + | PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => false, } } @@ -914,7 +919,11 @@ fn is_valid_props_annotation(param: &PatternLike) -> bool { PatternLike::ArrayPattern(ap) => ap.type_annotation.as_deref(), PatternLike::AssignmentPattern(ap) => ap.type_annotation.as_deref(), PatternLike::RestElement(re) => re.type_annotation.as_deref(), - PatternLike::MemberExpression(_) => None, + PatternLike::MemberExpression(_) + | PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => None, }; let annot = match type_annotation { Some(val) => val, diff --git a/compiler/crates/react_compiler/src/entrypoint/validate_source_locations.rs b/compiler/crates/react_compiler/src/entrypoint/validate_source_locations.rs index a58ad9fa131..c785d713745 100644 --- a/compiler/crates/react_compiler/src/entrypoint/validate_source_locations.rs +++ b/compiler/crates/react_compiler/src/entrypoint/validate_source_locations.rs @@ -746,6 +746,10 @@ fn collect_original_pattern( locations, ); } + PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => {} } } @@ -1236,6 +1240,10 @@ fn collect_generated_pattern( collect_generated_expression(&m.object, locations); collect_generated_expression(&m.property, locations); } + PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => {} } } diff --git a/compiler/crates/react_compiler_ast/src/patterns.rs b/compiler/crates/react_compiler_ast/src/patterns.rs index 6ba30d63265..48e199fd447 100644 --- a/compiler/crates/react_compiler_ast/src/patterns.rs +++ b/compiler/crates/react_compiler_ast/src/patterns.rs @@ -16,6 +16,10 @@ pub enum PatternLike { RestElement(RestElement), // Expressions can appear in pattern positions (e.g., MemberExpression as LVal) MemberExpression(crate::expressions::MemberExpression), + TSAsExpression(crate::expressions::TSAsExpression), + TSSatisfiesExpression(crate::expressions::TSSatisfiesExpression), + TSNonNullExpression(crate::expressions::TSNonNullExpression), + TSTypeAssertion(crate::expressions::TSTypeAssertion), } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/compiler/crates/react_compiler_ast/src/visitor.rs b/compiler/crates/react_compiler_ast/src/visitor.rs index 294cac2571e..21dafa6dbfe 100644 --- a/compiler/crates/react_compiler_ast/src/visitor.rs +++ b/compiler/crates/react_compiler_ast/src/visitor.rs @@ -589,6 +589,14 @@ impl<'a> AstWalker<'a> { self.walk_expression(v, &node.property); } } + PatternLike::TSAsExpression(node) => self.walk_expression(v, &node.expression), + PatternLike::TSSatisfiesExpression(node) => { + self.walk_expression(v, &node.expression) + } + PatternLike::TSNonNullExpression(node) => { + self.walk_expression(v, &node.expression) + } + PatternLike::TSTypeAssertion(node) => self.walk_expression(v, &node.expression), } } diff --git a/compiler/crates/react_compiler_lowering/src/build_hir.rs b/compiler/crates/react_compiler_lowering/src/build_hir.rs index c2920573d3f..5cb73df5e79 100644 --- a/compiler/crates/react_compiler_lowering/src/build_hir.rs +++ b/compiler/crates/react_compiler_lowering/src/build_hir.rs @@ -57,6 +57,10 @@ fn pattern_like_loc( PatternLike::AssignmentPattern(p) => p.base.loc.clone(), PatternLike::RestElement(p) => p.base.loc.clone(), PatternLike::MemberExpression(p) => p.base.loc.clone(), + PatternLike::TSAsExpression(p) => p.base.loc.clone(), + PatternLike::TSSatisfiesExpression(p) => p.base.loc.clone(), + PatternLike::TSNonNullExpression(p) => p.base.loc.clone(), + PatternLike::TSTypeAssertion(p) => p.base.loc.clone(), } } @@ -2258,6 +2262,10 @@ fn pattern_declares_name(pattern: &react_compiler_ast::patterns::PatternLike, na PatternLike::AssignmentPattern(ap) => pattern_declares_name(&ap.left, name), PatternLike::RestElement(r) => pattern_declares_name(&r.argument, name), PatternLike::MemberExpression(_) => false, + PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => false, } } @@ -2460,6 +2468,10 @@ fn collect_binding_names_from_pattern( collect_binding_names_from_pattern(&rest.argument, scope_id, scope_info, out); } PatternLike::MemberExpression(_) => {} + PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => {} } } @@ -4150,7 +4162,7 @@ pub fn lower( .unwrap_or(scope_info.program_scope); // Pre-compute context identifiers: variables captured across function boundaries - let context_identifiers = find_context_identifiers(func, scope_info); + let context_identifiers = find_context_identifiers(func, scope_info, env)?; // Build identifier location index from the AST (replaces serialized referenceLocs/jsxReferencePositions) let identifier_locs = build_identifier_loc_index(func, scope_info); @@ -5011,6 +5023,14 @@ fn lower_assignment( assignment_style, )?) } + + // TS assignment-target wrappers (e.g. `(x as T) = ...`). The TS-faithful + // Todo is recorded once in `find_context_identifiers`; lowering itself + // never reaches a successful path for these, so do not record again. + PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => Ok(None), } } @@ -5940,6 +5960,10 @@ fn lower_inner( }), ); } + react_compiler_ast::patterns::PatternLike::TSAsExpression(_) + | react_compiler_ast::patterns::PatternLike::TSSatisfiesExpression(_) + | react_compiler_ast::patterns::PatternLike::TSNonNullExpression(_) + | react_compiler_ast::patterns::PatternLike::TSTypeAssertion(_) => {} } } diff --git a/compiler/crates/react_compiler_lowering/src/find_context_identifiers.rs b/compiler/crates/react_compiler_lowering/src/find_context_identifiers.rs index 91c70b14c94..9d7e34e24d7 100644 --- a/compiler/crates/react_compiler_lowering/src/find_context_identifiers.rs +++ b/compiler/crates/react_compiler_lowering/src/find_context_identifiers.rs @@ -13,6 +13,12 @@ use react_compiler_ast::scope::*; use react_compiler_ast::statements::FunctionDeclaration; use react_compiler_ast::visitor::AstWalker; use react_compiler_ast::visitor::Visitor; +use react_compiler_diagnostics::CompilerError; +use react_compiler_diagnostics::CompilerErrorDetail; +use react_compiler_diagnostics::ErrorCategory; +use react_compiler_diagnostics::Position; +use react_compiler_diagnostics::SourceLocation; +use react_compiler_hir::environment::Environment; use crate::FunctionNode; @@ -25,10 +31,12 @@ struct BindingInfo { struct ContextIdentifierVisitor<'a> { scope_info: &'a ScopeInfo, + env: &'a mut Environment, /// Stack of inner function scopes encountered during traversal. /// Empty when at the top level of the function being compiled. function_stack: Vec, binding_info: HashMap, + error: Option, } impl<'a> ContextIdentifierVisitor<'a> { @@ -146,7 +154,11 @@ impl<'ast> Visitor<'ast> for ContextIdentifierVisitor<'_> { .last() .copied() .unwrap_or(self.scope_info.program_scope); - walk_lval_for_reassignment(self, &node.left, current_scope); + if self.error.is_none() { + if let Err(error) = walk_lval_for_reassignment(self, &node.left, current_scope) { + self.error = Some(error); + } + } } fn enter_update_expression(&mut self, node: &'ast UpdateExpression, scope_stack: &[ScopeId]) { @@ -165,7 +177,7 @@ fn walk_lval_for_reassignment( visitor: &mut ContextIdentifierVisitor<'_>, pattern: &PatternLike, current_scope: ScopeId, -) { +) -> Result<(), CompilerError> { match pattern { PatternLike::Identifier(ident) => { visitor.handle_reassignment_identifier(&ident.name, current_scope); @@ -173,7 +185,7 @@ fn walk_lval_for_reassignment( PatternLike::ArrayPattern(pat) => { for element in &pat.elements { if let Some(el) = element { - walk_lval_for_reassignment(visitor, el, current_scope); + walk_lval_for_reassignment(visitor, el, current_scope)?; } } } @@ -181,24 +193,89 @@ fn walk_lval_for_reassignment( for prop in &pat.properties { match prop { ObjectPatternProperty::ObjectProperty(p) => { - walk_lval_for_reassignment(visitor, &p.value, current_scope); + walk_lval_for_reassignment(visitor, &p.value, current_scope)?; } ObjectPatternProperty::RestElement(p) => { - walk_lval_for_reassignment(visitor, &p.argument, current_scope); + walk_lval_for_reassignment(visitor, &p.argument, current_scope)?; } } } } PatternLike::AssignmentPattern(pat) => { - walk_lval_for_reassignment(visitor, &pat.left, current_scope); + walk_lval_for_reassignment(visitor, &pat.left, current_scope)?; } PatternLike::RestElement(pat) => { - walk_lval_for_reassignment(visitor, &pat.argument, current_scope); + walk_lval_for_reassignment(visitor, &pat.argument, current_scope)?; } PatternLike::MemberExpression(_) => { // Interior mutability - not a variable reassignment } + PatternLike::TSAsExpression(node) => { + record_unsupported_lval(visitor.env, "TSAsExpression", convert_opt_loc(&node.base.loc))?; + } + PatternLike::TSSatisfiesExpression(node) => { + record_unsupported_lval( + visitor.env, + "TSSatisfiesExpression", + convert_opt_loc(&node.base.loc), + )?; + } + PatternLike::TSNonNullExpression(node) => { + record_unsupported_lval( + visitor.env, + "TSNonNullExpression", + convert_opt_loc(&node.base.loc), + )?; + } + PatternLike::TSTypeAssertion(node) => { + record_unsupported_lval( + visitor.env, + "TSTypeAssertion", + convert_opt_loc(&node.base.loc), + )?; + } } + Ok(()) +} + +fn convert_loc(loc: &react_compiler_ast::common::SourceLocation) -> SourceLocation { + SourceLocation { + start: Position { + line: loc.start.line, + column: loc.start.column, + index: loc.start.index, + }, + end: Position { + line: loc.end.line, + column: loc.end.column, + index: loc.end.index, + }, + } +} + +fn convert_opt_loc( + loc: &Option, +) -> Option { + loc.as_ref().map(convert_loc) +} + +/// Record the TS-faithful Todo for an unsupported assignment-target wrapper node, +/// mirroring the TypeScript `FindContextIdentifiers` pass. Recorded via the +/// environment's Todo mechanism (non-fatal under panicThreshold "none"). +fn record_unsupported_lval( + env: &mut Environment, + type_name: &str, + loc: Option, +) -> Result<(), CompilerError> { + env.record_error(CompilerErrorDetail { + category: ErrorCategory::Todo, + reason: format!( + "[FindContextIdentifiers] Cannot handle Object destructuring assignment target {type_name}" + ), + description: None, + loc, + suggestions: None, + }) } /// Check if a binding declared at `binding_scope` is captured by a function at `function_scope`. @@ -269,7 +346,8 @@ fn build_declaration_positions(scope_info: &ScopeInfo) -> HashSet<(BindingId, u3 pub fn find_context_identifiers( func: &FunctionNode<'_>, scope_info: &ScopeInfo, -) -> HashSet { + env: &mut Environment, +) -> Result, CompilerError> { let func_start = match func { FunctionNode::FunctionDeclaration(d) => d.base.start.unwrap_or(0), FunctionNode::FunctionExpression(e) => e.base.start.unwrap_or(0), @@ -283,8 +361,10 @@ pub fn find_context_identifiers( let mut visitor = ContextIdentifierVisitor { scope_info, + env, function_stack: Vec::new(), binding_info: HashMap::new(), + error: None, }; let mut walker = AstWalker::with_initial_scope(scope_info, func_scope); @@ -317,6 +397,10 @@ pub fn find_context_identifiers( } } + if let Some(error) = visitor.error { + return Err(error); + } + // Supplement the walker-based analysis with referenceToBinding data. // The AST walker doesn't visit identifiers inside type annotations, // but Babel's traverse (used by TS findContextIdentifiers) does. @@ -363,12 +447,12 @@ pub fn find_context_identifiers( } // Collect results - visitor + Ok(visitor .binding_info .into_iter() .filter(|(_, info)| { info.reassigned_by_inner_fn || (info.reassigned && info.referenced_by_inner_fn) }) .map(|(id, _)| id) - .collect() + .collect()) } diff --git a/compiler/crates/react_compiler_oxc/src/convert_ast.rs b/compiler/crates/react_compiler_oxc/src/convert_ast.rs index f25902f2279..bf9de9b42ff 100644 --- a/compiler/crates/react_compiler_oxc/src/convert_ast.rs +++ b/compiler/crates/react_compiler_oxc/src/convert_ast.rs @@ -2633,6 +2633,10 @@ impl<'a> ConvertCtx<'a> { rest.type_annotation = Some(type_json); } PatternLike::MemberExpression(_) => {} + PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => {} } } diff --git a/compiler/crates/react_compiler_oxc/src/convert_ast_reverse.rs b/compiler/crates/react_compiler_oxc/src/convert_ast_reverse.rs index c147427475b..1a5de6bdb95 100644 --- a/compiler/crates/react_compiler_oxc/src/convert_ast_reverse.rs +++ b/compiler/crates/react_compiler_oxc/src/convert_ast_reverse.rs @@ -1152,7 +1152,11 @@ impl<'a> ReverseCtx<'a> { .binding_pattern_assignment_pattern(SPAN, left, right) } PatternLike::RestElement(r) => self.convert_pattern_to_binding_pattern(&r.argument), - PatternLike::MemberExpression(_) => self + PatternLike::MemberExpression(_) + | PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => self .builder .binding_pattern_binding_identifier(SPAN, self.atom("__member_pattern__")), } @@ -1278,6 +1282,16 @@ impl<'a> ReverseCtx<'a> { self.convert_pattern_to_assignment_target(&ap.left) } PatternLike::RestElement(r) => self.convert_pattern_to_assignment_target(&r.argument), + PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => self + .builder + .simple_assignment_target_assignment_target_identifier( + SPAN, + self.atom("__unknown__"), + ) + .into(), } } diff --git a/compiler/crates/react_compiler_swc/src/convert_ast_reverse.rs b/compiler/crates/react_compiler_swc/src/convert_ast_reverse.rs index 944965da4a2..56529a231df 100644 --- a/compiler/crates/react_compiler_swc/src/convert_ast_reverse.rs +++ b/compiler/crates/react_compiler_swc/src/convert_ast_reverse.rs @@ -1563,6 +1563,20 @@ impl ReverseCtx { // MemberExpression in pattern position - convert to an expression pattern Pat::Expr(Box::new(self.convert_member_expression(m))) } + // TS wrappers in pattern position: strip the type wrapper, keep the + // inner expression (unreachable for unsupported targets; non-panicking). + PatternLike::TSAsExpression(e) => { + Pat::Expr(Box::new(self.convert_expression(&e.expression))) + } + PatternLike::TSSatisfiesExpression(e) => { + Pat::Expr(Box::new(self.convert_expression(&e.expression))) + } + PatternLike::TSNonNullExpression(e) => { + Pat::Expr(Box::new(self.convert_expression(&e.expression))) + } + PatternLike::TSTypeAssertion(e) => { + Pat::Expr(Box::new(self.convert_expression(&e.expression))) + } } } @@ -1607,6 +1621,12 @@ impl ReverseCtx { self.convert_pattern_to_assign_target(&ap.left) } PatternLike::RestElement(r) => self.convert_pattern_to_assign_target(&r.argument), + PatternLike::TSAsExpression(_) + | PatternLike::TSSatisfiesExpression(_) + | PatternLike::TSNonNullExpression(_) + | PatternLike::TSTypeAssertion(_) => AssignTarget::Simple(SimpleAssignTarget::Ident( + self.binding_ident("__unknown__", DUMMY_SP), + )), } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.expect.md index de5e29e0f39..71c9cddf69f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-rust-as-expression-assignment-target.expect.md @@ -18,7 +18,17 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Failed to parse AST JSON: unknown variant `TSAsExpression`, expected one of `Identifier`, `ObjectPattern`, `ArrayPattern`, `AssignmentPattern`, `RestElement`, `MemberExpression` at line 1 column 9800 +Found 1 error: + +Todo: [FindContextIdentifiers] Cannot handle Object destructuring assignment target TSAsExpression + +error.todo-rust-as-expression-assignment-target.ts:2:2 + 1 | function Component(props: {obj: {x: unknown}}) { +> 2 | (props.obj.x as unknown as number) = 1; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [FindContextIdentifiers] Cannot handle Object destructuring assignment target TSAsExpression + 3 | return
{props.obj.x as number}
; + 4 | } + 5 | ``` \ No newline at end of file