mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 13:21:55 +00:00
refactor(plan): update model path and enhance Babel plugin for ask262Debug
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
model: mimo-v2-pro
|
||||
model: accounts/fireworks/routers/kimi-k2p5-turbo
|
||||
---
|
||||
|
||||
# Plan: `ask262Debug` Module for engine262
|
||||
@@ -8,30 +8,49 @@ model: mimo-v2-pro
|
||||
|
||||
Runtime execution tracker that records which ECMAScript spec sections are entered during execution. The Babel transform auto-injects `ask262Debug.mark()` at the start of every function body with URL comments — developer writes nothing. All metadata (sectionId, file, line) resolved at build time.
|
||||
|
||||
**Primary Use Case:** AI analysis of execution flow mapped to ECMAScript spec sections for understanding JavaScript engine behavior.
|
||||
|
||||
## Files to Change
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `engine262/src/ask262-debug.mts` | **CREATE** — the module |
|
||||
| `engine262/src/index.mts` | **MODIFY** — add re-export |
|
||||
| `engine262/tsconfig.base.json` | **MODIFY** — add `#self/ask262Debug` path |
|
||||
| `engine262/src/ask262-debug.mts` | **CREATE** — the module with deduplication logic |
|
||||
| `engine262/src/index.mts` | **MODIFY** — add named re-export |
|
||||
| `engine262/tsconfig.base.json` | **MODIFY** — add `#self/ask262Debug` path mapping |
|
||||
| `engine262/package.json` | **MODIFY** — add `#self/ask262Debug` import |
|
||||
| `engine262/scripts/rollup.config.mts` | **MODIFY** — extend `resolve-self` plugin |
|
||||
| `engine262/scripts/transform.mts` | **MODIFY** — auto-inject `mark()` into function bodies |
|
||||
| `engine262/scripts/rollup.config.mts` | **MODIFY** — add resolve-self plugin to main build, extend for subpaths |
|
||||
| `engine262/scripts/transform.mts` | **MODIFY** — auto-inject `mark()` and import |
|
||||
| `engine262/scripts/verify-ask262-debug.mts` | **CREATE** — post-build verification script |
|
||||
|
||||
## 1. `scripts/transform.mts` — Enhance the Babel plugin
|
||||
## 1. `scripts/transform.mts` — Enhance the Babel Plugin
|
||||
|
||||
### 1a. Refactor `addSectionFromComments` to also inject `mark()` into body
|
||||
|
||||
Current behavior: sets `FuncName.section = 'url'` after the declaration.
|
||||
New behavior: also injects `ask262Debug.mark(["sec-..."], "file.mts", lineNumber)` as the first statement inside the function body.
|
||||
### 1a. Extend State Interface
|
||||
|
||||
```typescript
|
||||
function addSectionFromComments(path, getName) {
|
||||
type NeededNames = 'Completion' | 'AbruptCompletion' | 'Assert' | 'Call' |
|
||||
'IteratorClose' | 'AsyncIteratorClose' | 'Value' |
|
||||
'skipDebugger' | 'ask262Debug'; // NEW
|
||||
|
||||
interface State extends PluginPass {
|
||||
needed: Partial<Record<NeededNames, boolean>>;
|
||||
fileRelativePath?: string; // NEW: captured at Program enter
|
||||
}
|
||||
```
|
||||
|
||||
### 1b. Refactor `addSectionFromComments`
|
||||
|
||||
```typescript
|
||||
function addSectionFromComments(
|
||||
path: NodePath<t.FunctionDeclaration> | NodePath<t.VariableDeclaration> |
|
||||
NodePath<t.ExportNamedDeclaration> | NodePath<t.ClassMethod> | NodePath<t.ObjectMethod>,
|
||||
state: State,
|
||||
getName: () => string
|
||||
) {
|
||||
if (!path.node.leadingComments) return;
|
||||
|
||||
const sectionIds = [];
|
||||
const sectionIds: string[] = [];
|
||||
let url = '';
|
||||
|
||||
for (const c of path.node.leadingComments) {
|
||||
for (const line of c.value.split('\n')) {
|
||||
const matches = line.match(/#(sec-[a-zA-Z0-9._%-]+)/g);
|
||||
@@ -39,110 +58,204 @@ function addSectionFromComments(path, getName) {
|
||||
sectionIds.push(...matches.map(m => m.substring(1)));
|
||||
if (!url) {
|
||||
const section = line.split(' ').find(l => l.includes('#sec'));
|
||||
if (section) {
|
||||
url = section.includes('https') ? section : `https://tc39.es/ecma262/${section}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionIds.length === 0) return;
|
||||
|
||||
const name = getName();
|
||||
|
||||
// 1. Keep existing .section (backward compat)
|
||||
// 1. Keep existing .section (backward compat) - uses first URL
|
||||
if (url) {
|
||||
path.insertAfter(template.ast(`${name}.section = '${url}';`));
|
||||
}
|
||||
|
||||
// 2. Inject mark() at start of function body
|
||||
const body = path.node.body;
|
||||
// 2. Determine if we can inject mark()
|
||||
let body: t.BlockStatement | null = null;
|
||||
|
||||
if (path.isFunctionDeclaration()) {
|
||||
body = path.node.body;
|
||||
} else if (path.isClassMethod() || path.isObjectMethod()) {
|
||||
body = path.node.body;
|
||||
} else if (path.isVariableDeclaration()) {
|
||||
const init = (path.get('declarations.0') as NodePath<t.VariableDeclarator>).get('init');
|
||||
if (init.isFunctionExpression()) {
|
||||
body = init.node.body;
|
||||
} else if (init.isArrowFunctionExpression()) {
|
||||
body = init.node.body.type === 'BlockStatement' ? init.node.body : null;
|
||||
}
|
||||
} else if (path.isExportNamedDeclaration()) {
|
||||
const declaration = path.node.declaration;
|
||||
if (declaration?.type === 'FunctionDeclaration') {
|
||||
body = declaration.body;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Inject mark() at start of function body (only for block bodies)
|
||||
if (body?.type === 'BlockStatement') {
|
||||
const relativeFile = getRelativeFilePath(state);
|
||||
const line = path.node.loc?.start.line ?? 0;
|
||||
const markCall = template.ast(
|
||||
'ask262Debug.mark(SECTION_IDS, FILE, LINE);'
|
||||
.replace('SECTION_IDS', JSON.stringify(sectionIds))
|
||||
.replace('FILE', JSON.stringify(relativeFile))
|
||||
.replace('LINE', String(line))
|
||||
);
|
||||
const markCall = template.statement(
|
||||
`ask262Debug.mark(${JSON.stringify(sectionIds)}, ${JSON.stringify(state.fileRelativePath)}, ${line});`
|
||||
)();
|
||||
body.body.unshift(markCall);
|
||||
state.needed.ask262Debug = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1b. Add visitors for all function-like nodes
|
||||
### 1c. Add Visitors for All Function-like Nodes
|
||||
|
||||
```typescript
|
||||
FunctionDeclaration(path) {
|
||||
addSectionFromComments(path, () => path.node.id!.name);
|
||||
},
|
||||
ClassMethod(path) {
|
||||
addSectionFromComments(path, () => (path.node.key as t.Identifier).name);
|
||||
},
|
||||
ObjectMethod(path) {
|
||||
addSectionFromComments(path, () => (path.node.key as t.Identifier).name);
|
||||
},
|
||||
// Existing visitors (VariableDeclaration, ExportNamedDeclaration) unchanged
|
||||
return {
|
||||
visitor: {
|
||||
Program: {
|
||||
enter(path, state) {
|
||||
state.needed = {};
|
||||
// Capture relative file path from state.filename
|
||||
const absolutePath = state.filename || '';
|
||||
state.fileRelativePath = absolutePath.replace(/.*\/(src\/)/, '$1').replace(/\.mts$/, '.mts') || 'unknown.mts';
|
||||
},
|
||||
exit(path, state) {
|
||||
// Existing imports...
|
||||
if (state.needed.ask262Debug) {
|
||||
path.unshiftContainer('body', template.ast(`import ask262Debug from '#self/ask262Debug';`));
|
||||
}
|
||||
},
|
||||
},
|
||||
FunctionDeclaration(path, state) {
|
||||
addSectionFromComments(path, state, () => path.node.id!.name);
|
||||
},
|
||||
VariableDeclaration(path, state) {
|
||||
const init = path.get('declarations.0.init');
|
||||
if (init.isFunctionExpression() || init.isArrowFunctionExpression()) {
|
||||
const id = path.node.declarations[0].id as t.Identifier;
|
||||
addSectionFromComments(path, state, () => id.name);
|
||||
}
|
||||
},
|
||||
ExportNamedDeclaration(path, state) {
|
||||
const declaration = path.node.declaration;
|
||||
if (declaration?.type === 'FunctionDeclaration') {
|
||||
addSectionFromComments(path, state, () => declaration.id!.name);
|
||||
}
|
||||
},
|
||||
ClassMethod(path, state) {
|
||||
const key = path.node.key;
|
||||
if (key.type === 'Identifier') {
|
||||
addSectionFromComments(path, state, () => key.name);
|
||||
}
|
||||
},
|
||||
ObjectMethod(path, state) {
|
||||
const key = path.node.key;
|
||||
if (key.type === 'Identifier') {
|
||||
addSectionFromComments(path, state, () => key.name);
|
||||
}
|
||||
},
|
||||
// ... rest of existing visitors (CallExpression, ThrowStatement, etc.) unchanged
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 1c. Multi-section support
|
||||
### 1d. Arrow Function Special Handling
|
||||
|
||||
Functions with multiple URL comments get all section IDs as an array:
|
||||
The updated `addSectionFromComments` handles this automatically:
|
||||
- Arrow with block body `() => { ... }`: `.section` + `mark()` injected
|
||||
- Arrow with expression body `() => expr`: `.section` only, no `mark()`
|
||||
|
||||
```typescript
|
||||
/** https://tc39.es/ecma262/#sec-array.prototype.every */
|
||||
/** https://tc39.es/ecma262/#sec-%typedarray%.prototype.every */
|
||||
function* ArrayProto_every(args) { ... }
|
||||
## 2. `engine262/package.json` — Add Subpath Import
|
||||
|
||||
// Transform injects:
|
||||
function* ArrayProto_every(args) {
|
||||
ask262Debug.mark(["sec-array.prototype.every", "sec-%typedarray%.prototype.every"], "intrinsics/ArrayPrototypeShared.mts", 173);
|
||||
...
|
||||
```json
|
||||
{
|
||||
"imports": {
|
||||
"#self": "./lib/engine262.mjs",
|
||||
"#self/ask262Debug": "./src/ask262-debug.mts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. `engine262/package.json` — Add subpath import
|
||||
## 3. `engine262/tsconfig.base.json` — Add Path Mapping
|
||||
|
||||
```json
|
||||
"#self/ask262Debug": "./src/ask262-debug.mts"
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"#self": ["./lib/engine262.mjs"],
|
||||
"#self/ask262Debug": ["./src/ask262-debug.mts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. `engine262/tsconfig.base.json` — Add path mapping
|
||||
## 4. `engine262/scripts/rollup.config.mts` — Add resolve-self to Main Build
|
||||
|
||||
```json
|
||||
"#self/ask262Debug": ["./src/ask262-debug.mts"]
|
||||
```
|
||||
|
||||
## 4. `scripts/rollup.config.mts` — Extend resolve-self plugin
|
||||
|
||||
Change from exact `#self` match to prefix `#self/` match in both builds:
|
||||
### 4a. Extend Existing resolve-self Plugin
|
||||
|
||||
```typescript
|
||||
resolveId(source, _importer, _options) {
|
||||
if (source === '#self' || source.startsWith('#self/')) {
|
||||
return { id: './engine262.mjs' } as any; // inspector build
|
||||
// Inspector build (existing plugin, modified)
|
||||
{
|
||||
name: 'resolve-self',
|
||||
resolveId(source, _importer, _options) {
|
||||
if (source === '#self') {
|
||||
return { id: './engine262.mjs' };
|
||||
}
|
||||
if (source.startsWith('#self/')) {
|
||||
// For inspector build, resolve subpaths to bundled engine262
|
||||
return { id: './engine262.mjs' }; // All #self/* resolve to main bundle
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Add similar plugin to main engine262 build (which currently lacks one):
|
||||
### 4b. Add to Main Engine262 Build
|
||||
|
||||
```typescript
|
||||
{
|
||||
input: './src/index.mts',
|
||||
plugins: [
|
||||
importUnicodeLib(),
|
||||
(json.default || json)({ compact: true }),
|
||||
(commonjs.default || commonjs)(),
|
||||
nodeResolve({ exportConditions: ['rollup'], extensions: ['.mts'] }),
|
||||
{
|
||||
name: 'resolve-self',
|
||||
resolveId(source) {
|
||||
if (source === '#self' || source.startsWith('#self/')) return { id: '#self' };
|
||||
if (source === '#self') {
|
||||
return { id: '#self' }; // Mark as internal, bundled
|
||||
}
|
||||
if (source === '#self/ask262Debug') {
|
||||
return { id: './src/ask262-debug.mts' }; // Will be bundled
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
babel({
|
||||
...babelOptions,
|
||||
plugins: [
|
||||
'./scripts/transform.mts',
|
||||
['@babel/plugin-proposal-decorators', { 'version': '2023-11' }],
|
||||
],
|
||||
}),
|
||||
// ... dts plugin
|
||||
],
|
||||
// ... output config
|
||||
}
|
||||
```
|
||||
|
||||
## 5. `engine262/src/index.mts` — Add re-export
|
||||
## 5. `engine262/src/index.mts` — Add Named Re-export
|
||||
|
||||
```typescript
|
||||
// Add to existing exports
|
||||
export { default as ask262Debug, type MarkData } from './ask262-debug.mts';
|
||||
```
|
||||
|
||||
## 6. `engine262/src/ask262-debug.mts` — The module
|
||||
## 6. `engine262/src/ask262-debug.mts` — The Module
|
||||
|
||||
Pure data collection. No stack parsing, no `node:` imports, no decorators.
|
||||
Deduplicated data collection. No stack parsing, no `node:` imports, no decorators.
|
||||
|
||||
```typescript
|
||||
export interface MarkData {
|
||||
@@ -152,17 +265,43 @@ export interface MarkData {
|
||||
readonly important: boolean;
|
||||
}
|
||||
|
||||
interface MarkKey {
|
||||
readonly sectionIds: string;
|
||||
readonly file: string;
|
||||
readonly line: number;
|
||||
}
|
||||
|
||||
class Ask262Debug {
|
||||
marks: MarkData[] = [];
|
||||
private _importantStack = false;
|
||||
private _markIndex = new Map<string, number>(); // key -> index in marks
|
||||
|
||||
private _makeKey(sectionIds: string[], file: string, line: number): string {
|
||||
return `${sectionIds.join(',')}|${file}|${line}`;
|
||||
}
|
||||
|
||||
mark(sectionIds: string[], file: string, line: number) {
|
||||
this.marks.push({
|
||||
sectionIds,
|
||||
const key = this._makeKey(sectionIds, file, line);
|
||||
const existingIndex = this._markIndex.get(key);
|
||||
|
||||
if (existingIndex !== undefined) {
|
||||
// Merge important flag
|
||||
const existing = this.marks[existingIndex];
|
||||
if (this._importantStack && !existing.important) {
|
||||
(this.marks[existingIndex] as any).important = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const newMark: MarkData = {
|
||||
sectionIds: [...sectionIds], // defensive copy
|
||||
fileRelativePath: file,
|
||||
lineNumber: line,
|
||||
important: this._importantStack,
|
||||
});
|
||||
};
|
||||
|
||||
this._markIndex.set(key, this.marks.length);
|
||||
this.marks.push(newMark);
|
||||
}
|
||||
|
||||
startImportant() {
|
||||
@@ -178,47 +317,111 @@ const ask262Debug = new Ask262Debug();
|
||||
export default ask262Debug;
|
||||
```
|
||||
|
||||
### Developer writes
|
||||
### Key Behaviors
|
||||
|
||||
Nothing. The transform auto-injects `mark()` into every function body with URL comments.
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| Duplicate `(sectionIds, file, line)` | Keep one entry, merge `important` flags (OR logic) |
|
||||
| Generator resumption | Mark on every `.next()` call (may create duplicates if same location) |
|
||||
| Nested functions | Each entry adds its mark (chronological trace) |
|
||||
|
||||
### What the transform produces
|
||||
## 7. `engine262/scripts/verify-ask262-debug.mts` — Verification Script
|
||||
|
||||
```typescript
|
||||
// Original:
|
||||
/** https://tc39.es/ecma262/#sec-proxycreate */
|
||||
export function ProxyCreate(target, handler) {
|
||||
// ...
|
||||
#!/usr/bin/env node
|
||||
import engine262 from '../lib/engine262.mjs';
|
||||
|
||||
console.log('=== ask262Debug Verification ===\n');
|
||||
|
||||
// 1. Check export exists
|
||||
console.log('1. Checking export...');
|
||||
if (!engine262.ask262Debug) {
|
||||
console.error('FAIL: ask262Debug not exported');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(' ✓ ask262Debug exported\n');
|
||||
|
||||
// 2. Run code that hits known spec sections
|
||||
console.log('2. Executing test code...');
|
||||
const agent = new engine262.Agent();
|
||||
const realm = new engine262.ManagedRealm({ agent });
|
||||
|
||||
realm.evaluateScript(`
|
||||
// Array.prototype.every - should hit sec-array.prototype.every
|
||||
[1, 2, 3].every(x => x > 0);
|
||||
|
||||
// Proxy creation - should hit sec-proxycreate or similar
|
||||
new Proxy({}, {});
|
||||
`);
|
||||
|
||||
// 3. Verify marks were captured
|
||||
console.log('3. Checking marks...');
|
||||
const marks = engine262.ask262Debug.marks;
|
||||
console.log(` Captured ${marks.length} unique marks\n`);
|
||||
|
||||
if (marks.length === 0) {
|
||||
console.error('FAIL: No marks captured');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Transformed:
|
||||
export function ProxyCreate(target, handler) {
|
||||
ask262Debug.mark(["sec-proxycreate"], "abstract-ops/proxy-objects.mts", 540);
|
||||
// ...
|
||||
}
|
||||
ProxyCreate.section = 'https://tc39.es/ecma262/#sec-proxycreate';
|
||||
// 4. Show sample marks
|
||||
console.log('4. Sample marks:');
|
||||
marks.slice(0, 5).forEach((m, i) => {
|
||||
console.log(` [${i}] ${m.sectionIds.join(', ')} @ ${m.fileRelativePath}:${m.lineNumber}${m.important ? ' [important]' : ''}`);
|
||||
});
|
||||
|
||||
console.log('\n=== All Checks Passed ===');
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
## Key Design Decisions Summary
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Module name | `#self/ask262Debug` | Follows existing `#self` pattern |
|
||||
| Export style | Singleton class, default export | `ask262Debug.marks` access |
|
||||
| `MarkData` type | Named export | Users can type-annotate |
|
||||
| All metadata | From transform (build time) | No runtime stack parsing, no `node:` imports |
|
||||
| `mark()` injection | Auto-injected at start of function body | Developer writes nothing |
|
||||
| Multi-section | Array of section IDs per function | Functions can implement multiple spec sections |
|
||||
| `markLine()` | Removed | `mark()` covers all cases |
|
||||
| `.section` | Kept (backward compat) | Used by test262-intrinsics |
|
||||
| `important` | Stubbed with boolean flag | `startImportant()`/`stopImportant()` |
|
||||
| Function types | FunctionDeclaration + ClassMethod + ObjectMethod | Covers all URL comment patterns |
|
||||
| **Use case** | AI execution flow analysis | Map runtime to spec sections |
|
||||
| **Data structure** | Deduplicated array | Chronological trace with unique entries only |
|
||||
| **Deduplication key** | `(sectionIds, file, line)` | Same location = same semantic entry |
|
||||
| **Duplicate handling** | Merge `important` flags (OR) | If any call was important, mark it important |
|
||||
| **Module name** | `#self/ask262Debug` | Follows existing `#self` pattern |
|
||||
| **Export style** | Named export from engine262 | `import { ask262Debug } from '#self'` |
|
||||
| **Bundle strategy** | Included in engine262.mjs | Single artifact, no external dependency |
|
||||
| **All metadata** | From transform (build time) | No runtime stack parsing, no `node:` imports |
|
||||
| **`mark()` injection** | Auto-injected at start of block body | Developer writes nothing |
|
||||
| **Arrow functions** | Block body: `.section` + `mark()`, Expression: `.section` only | Can't inject statement in expression |
|
||||
| **Multi-section** | Array of section IDs per function | Functions can implement multiple spec sections |
|
||||
| **`.section`** | Kept (backward compat) | Used by test262-intrinsics |
|
||||
| **Generators** | Mark on every `.next()` | Simpler, deduplication handles repeats |
|
||||
| **Nested functions** | Mark all | Chronological trace for AI analysis |
|
||||
| **`important`** | User-driven `startImportant()`/`stopImportant()` | Manual annotation of interesting sections |
|
||||
| **Function types** | FunctionDeclaration + ClassMethod + ObjectMethod + VariableDeclaration | Covers all URL comment patterns |
|
||||
| **Class/Object method keys** | Identifier names only | Simple, covers 99% of cases |
|
||||
| **File paths** | Relative to `engine262/src/`, `.mts` extension | Maps to source, not compiled output |
|
||||
| **Line numbers** | 1-indexed from Babel AST | Matches source file line numbers |
|
||||
|
||||
## Verification
|
||||
## Verification Steps
|
||||
|
||||
1. `cd engine262 && npx tsc -b .` — types compile
|
||||
2. `cd engine262 && npm run lint` — no ESLint violations
|
||||
3. Verify transform output: check that functions with URL comments have `ask262Debug.mark(...)` injected at start of body
|
||||
4. Verify multi-section: check `ArrayProto_every` gets both section IDs
|
||||
5. Run `npm run build:engine` — rollup resolves `#self/ask262Debug` correctly
|
||||
6. Inspect `ask262Debug.marks` after running a test to verify marks are populated with correct data
|
||||
1. **Type checking**: `cd engine262 && npx tsc -b .`
|
||||
2. **Linting**: `cd engine262 && npm run lint`
|
||||
3. **Build**: `cd engine262 && npm run build:engine`
|
||||
4. **Verification**: `cd engine262 && node scripts/verify-ask262-debug.mts`
|
||||
5. **Manual inspection**: Check that transformed files have `ask262Debug.mark()` at start of functions with URL comments
|
||||
6. **Multi-section test**: Find a function with multiple URL comments (e.g., `ArrayProto_every`) and verify all section IDs are in the array
|
||||
|
||||
## Expected Output from Verification
|
||||
|
||||
```
|
||||
=== ask262Debug Verification ===
|
||||
|
||||
1. Checking export...
|
||||
✓ ask262Debug exported
|
||||
|
||||
2. Executing test code...
|
||||
3. Checking marks...
|
||||
Captured 47 unique marks
|
||||
|
||||
4. Sample marks:
|
||||
[0] sec-array.prototype.every @ intrinsics/ArrayPrototypeShared.mts:173
|
||||
[1] sec-proxycreate @ abstract-ops/proxy-objects.mts:540
|
||||
...
|
||||
|
||||
=== All Checks Passed ===
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user