Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Test

on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
workflow_dispatch:

jobs:
test-v:
name: V Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install V
uses: vlang/setup-v@v1.4

- name: Get V version
run: v --version

- name: Build gaslsp
run: |
cd src
v -o gaslsp .

- name: Run integration tests
run: |
mkdir -p ~/.local/bin
cp src/gaslsp ~/.local/bin/gaslsp
cp -r src/tables ~/.local/bin/
chmod +x tests/integration.sh tests/test_diags.sh
tests/integration.sh

test-diags:
name: Diagnostic Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install V
uses: vlang/setup-v@v1.4

- name: Build gaslsp and test_diags
run: |
cd src
v -o gaslsp .
cd ../tests
v -o test_diags test_diags.v

- name: Install and test diagnostics
run: |
mkdir -p ~/.local/bin
cp src/gaslsp ~/.local/bin/gaslsp
cp tests/test_diags ~/.local/bin/test_diags
cp -r src/tables ~/.local/bin/
chmod +x tests/test_diags.sh
WORKSPACE="$(pwd)" tests/test_diags.sh
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
node_modules
*.vsix

gaslsp
test_diags
6 changes: 5 additions & 1 deletion .vscodeignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
.vscode/**
.vscode/
.github/
.gitignore
.gitattributes
.editorconfig

src/
docs/
tests/
16 changes: 16 additions & 0 deletions docs/diagnostics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@ State Diagnostics (D034)
- Warning
- Register may be read before being written (uninitialized)

Statement Diagnostics (D020)
------------------------------

.. list-table::
:header-rows: 1
:widths: 15 15 70

* - Code
- Severity
- Description

* - D020
- Hint
- TODO/FIXME/HACK/XXX/BUG comment found

Configuration
--------------

Expand All @@ -173,3 +188,4 @@ Diagnostics can be suppressed or promoted to errors in ``gaslsp.toml``:
abi = true # D016-D017
symbol = true # D006-D008, D019
state = true # D034: uninitialized register tracking
statements = true # D020: TODO/FIXME comments
2 changes: 2 additions & 0 deletions docs/gaslsp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ operand = true # D004-D005, D009-D010, D018: operand issues
encoding = true # D011-D015: encoding issues
abi = true # D016-D017: ABI issues
symbol = true # D006-D008, D019: symbol issues
state = true # D034: uninitialized register tracking
statements = true # D020: TODO/FIXME comments

# Enable/disable severity levels
[diagnostics.levels]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "assembly-utils",
"displayName": "assembly-utils",
"description": "",
"version": "2.0.0",
"version": "2.0.1",
"publisher": "babywolf",
"repository": {
"url": "https://github.com/fgsoftware1/assembly-utils-vscode"
Expand Down
27 changes: 27 additions & 0 deletions src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Binaries for programs and plugins
main

*.exe
*.exe~
*.so
*.dylib
*.dll

# Ignore binary output folders
bin/

# Ignore common editor/system specific metadata
.DS_Store
.idea/
.vscode/
*.iml

# ENV
.env

# vweb and database
*.db
*.js

# Ignore installed modules through `v install --local`:
modules/
20 changes: 11 additions & 9 deletions src/config.v
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import os

pub struct DiagCategories {
pub mut:
size bool = true
truncation bool = true
register bool = true
symbol bool = true
directive bool = true
operand bool = true
encoding bool = true
abi bool = true
state bool = true
size bool = true
truncation bool = true
register bool = true
symbol bool = true
directive bool = true
operand bool = true
encoding bool = true
abi bool = true
state bool = true
statements bool = true
}

pub struct DiagLevels {
Expand Down Expand Up @@ -206,6 +207,7 @@ fn apply(key string, value string, section string, mut cfg Config) {
'encoding' { cfg.diagnostics.categories.encoding = v }
'abi' { cfg.diagnostics.categories.abi = v }
'state' { cfg.diagnostics.categories.state = v }
'statements' { cfg.diagnostics.categories.statements = v }
else {}
}
}
Expand Down
44 changes: 28 additions & 16 deletions src/diagnostics.v
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ pub fn (mut eng DiagEngine) publish_workspace() {
fn (eng &DiagEngine) check_line(l Line, raw string, globals map[string]bool, path string) []Diag {
mut diags := []Diag{}

// D031: incomplete label - line has label-like content but no colon
// D018 incomplete label - line has label-like content but no colon
stripped := raw.trim_space()
if stripped.len > 0 && !stripped.starts_with('.') && !stripped.starts_with('#') && !stripped.contains(':') {
first_word := stripped.split(' ')[0].split(' ')[0]
Expand All @@ -334,6 +334,18 @@ fn (eng &DiagEngine) check_line(l Line, raw string, globals map[string]bool, pat
}
}

// TODO check - comments containing TODO/FIXME/HACK/XXX
if eng.enabled('D020') && eng.cfg.diagnostics.categories.statements {
todo_patterns := ['TODO', 'FIXME', 'HACK', 'XXX', 'BUG']
upper := stripped.to_upper()
for pattern in todo_patterns {
if upper.contains(pattern) {
diags << eng.make(l, raw, 'D020', .hint, "TODO comment found: '${pattern}' - consider addressing this")
break
}
}
}

if l.kind != .instruction && l.kind != .label_and_instruction {
return diags
}
Expand Down Expand Up @@ -391,7 +403,7 @@ fn (eng &DiagEngine) check_size(l Line, raw string) []Diag {
return diags
}

// D004 truncation, D006 REX+high-byte, D021 32-bit mem in 64-bit
// D004 truncation, D005 REX+high-byte, D009 32-bit mem in 64-bit
fn (eng &DiagEngine) check_operands(l Line, raw string) []Diag {
mut diags := []Diag{}
suffix_bits := if l.suffix != 0 { suffix_width(l.suffix) } else { 0 }
Expand All @@ -418,7 +430,7 @@ fn (eng &DiagEngine) check_operands(l Line, raw string) []Diag {
}
}
.memory {
// D021 — 32-bit base register in memory operand
// D009 — 32-bit base register in memory operand
// crude check: look for (%e__) pattern
if op.raw.contains('(%e') && eng.enabled('D009') {
diags << eng.make(l, raw, 'D009', .error, "memory operand '${op.raw}' uses 32-bit base register in 64-bit mode; consider using the 64-bit equivalent")
Expand All @@ -428,7 +440,7 @@ fn (eng &DiagEngine) check_operands(l Line, raw string) []Diag {
}
}

// D022 src == dst — check once per instruction
// D010 src == dst — check once per instruction
if l.operands.len == 2 && l.mnemonic != 'xor' {
src, dst := l.operands[0], l.operands[1]
if src.kind == .register && dst.kind == .register && src.reg == dst.reg
Expand All @@ -440,31 +452,31 @@ fn (eng &DiagEngine) check_operands(l Line, raw string) []Diag {
return diags
}

// D023 div-by-immediate, D025 invalid operand size, D026/D027 mul/imul, D028 shift count
// D011 div-by-immediate, D012 pushb, D013 one-operand imul, D014 mul unsigned, D015 shift count
fn (eng &DiagEngine) check_encoding(l Line, raw string) []Diag {
mut diags := []Diag{}

match l.mnemonic {
'div', 'idiv' {
// D023 — div with immediate
// D011 — div with immediate
if l.operands.any(it.kind == .immediate) && eng.enabled('D011') {
diags << eng.make(l, raw, 'D011', .error, "'${l.mnemonic}' does not support immediate operands; load divisor into a register first")
}
}
'imul' {
// D026 — one-operand imul
// D013 — one-operand imul
if l.operands.len == 1 && eng.enabled('D013') {
diags << eng.make(l, raw, 'D013', .warning, "'imul' one-operand form: high half of result in rdx may be unexpected; did you want the two-operand form?")
}
}
'mul' {
// D027 — mul vs imul
// D014 — mul vs imul
if eng.enabled('D014') {
diags << eng.make(l, raw, 'D014', .warning, "'mul' is unsigned multiply; upper half stored in rdx may be silently discarded; use 'imul' if signed")
}
}
'shl', 'shr', 'sar' {
// D028 — shift count must be imm8 or %cl
// D015 — shift count must be imm8 or %cl
if l.operands.len >= 1 {
count := l.operands[0]
if count.kind == .register && count.reg != 'cl' && eng.enabled('D015') {
Expand All @@ -473,7 +485,7 @@ fn (eng &DiagEngine) check_encoding(l Line, raw string) []Diag {
}
}
'push' {
// D025 — pushb not encodable
// D012 — pushb not encodable
if l.suffix == `b` && eng.enabled('D012') {
diags << eng.make(l, raw, 'D012', .error, "'pushb' is not encodable; push only supports 16/32/64-bit operands")
}
Expand All @@ -484,7 +496,7 @@ fn (eng &DiagEngine) check_encoding(l Line, raw string) []Diag {
return diags
}

// D029 syscall clobber, D030 int $0x80
// D016 syscall clobber, D017 int $0x80
fn (eng &DiagEngine) check_abi(l Line, raw string) []Diag {
mut diags := []Diag{}
if !eng.cfg.diagnostics.categories.abi {
Expand All @@ -510,11 +522,11 @@ fn (eng &DiagEngine) check_abi(l Line, raw string) []Diag {
return diags
}

// D012 undefined symbol, D013 missing .global, D014 dead export, D015 duplicate
// D006 undefined symbol, D007 missing .global, D008 duplicate, D019 not exported
fn (eng &DiagEngine) check_symbols(path string, lines []string, globals map[string]bool) []Diag {
mut diags := []Diag{}

// D032: _start defined but not declared .global
// D019: _start defined but not declared .global
mut has_start := false
mut has_global_start := false
for i, raw in lines {
Expand Down Expand Up @@ -574,7 +586,7 @@ fn (eng &DiagEngine) check_symbols(path string, lines []string, globals map[stri
continue
}

// D013 — referenced cross-file but not .global
// D007 — referenced cross-file but not .global
if found.file != path && found.vis == .local {
if eng.enabled('D007') && eng.cfg.symbols.warn_missing_global {
diags << Diag{
Expand All @@ -592,9 +604,9 @@ fn (eng &DiagEngine) check_symbols(path string, lines []string, globals map[stri
}
}

// D014 dead exports — requires cross-file reference tracking (TODO)
// D014 dead exports — requires cross-file reference tracking (TODO)

// D015 duplicate symbols
// D008 duplicate symbols
for name, _ in globals {
syms := eng.get_indexer().index.find_all(name)
if syms.len > 1 && eng.enabled('D008') {
Expand Down
20 changes: 20 additions & 0 deletions src/tables/instrs.csv
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,23 @@ cqo,,,"Sign-extend %rax into %rdx:%rax. Use before idivq.",none,""
lock,,,"Bus lock prefix. Makes the following RMW instruction atomic.",none,"Only valid with: add sub and or xor xchg cmpxchg inc dec neg not."
rdtsc,,,"Read Time Stamp Counter. Result in %edx:%eax.",none,"%rcx clobbered. Not serializing."
cpuid,,,"CPU identification. Input in %eax (leaf). Output in %eax/%ebx/%ecx/%edx.",none,"Serializing instruction."
cli,,,"Clear Interrupt Flag. Disables maskable interrupts.",none,"Ring 0 only. Use 'sti' to re-enable."
sti,,,"Set Interrupt Flag. Enables maskable interrupts.",none,"Ring 0 only. Must follow a 'cli' with actual I/O."
pusha,,,"Push all general-purpose registers.",none,"Pushes: ax, cx, dx, bx, sp, bp, si, di. Order is undefined."
popa,,,"Pop all general-purpose registers.",none,"Pops: di, si, bp, sp, bx, dx, cx, ax. Ignores the popped value for sp."
wait,,,"Check for and process pending unmasked exceptions.",none,"Also used with FPU."
iret,,,"Interrupt Return. Pops IP, CS, and flags.",none,"Return from interrupt handler."
lgdt,,mem,"Load Global Descriptor Table Register.",none,"Loads GDT from memory. Operand is 10-byte pointer."
lidt,,mem,"Load Interrupt Descriptor Table Register.",none,"Loads IDT from memory. Operand is 10-byte pointer."
ltr,,reg,"Load Task Register.",none,"Loads task register from TR. Also sets the busy bit."
sgdt,,mem,"Store Global Descriptor Table Register.",none,"Stores GDT to memory. Returns 10-byte pointer."
sidt,,mem,"Store Interrupt Descriptor Table Register.",none,"Stores IDT to memory. Returns 10-byte pointer."
str,,reg,"Store Task Register.",none,"Stores current task register."
rdmsr,,,"Read Model Specific Register. Input in %ecx.",none,"Output in %edx:%eax. Ring 0 only."
wrmsr,,,"Write Model Specific Register. Input in %ecx and %edx:%eax.",none,"Ring 0 only."
rdtscp,,,"Read Time Stamp Counter and Processor Info.",none,"Result in %edx:%eax. %rcx clobbered. Serializing."
movq,_,cr,"Move to/from Control Register.",none,"cr0, cr2, cr3, cr4, cr8. Ring 0 only."
movq,_,dr,"Move to/from Debug Register.",none,"dr0-dr7. Ring 0 only."
invlpg,,mem,"Invalidate TLB Entry.",none,"Flushes single TLB entry. Ring 0 only."
invd,,,"Invalidate all caches.",none,"Flushes all caches and TLBs. Ring 0 only."
wbinvd,,,"Write back and invalidate caches.",none,"Writes back then flushes. Ring 0 only."
2 changes: 1 addition & 1 deletion syntaxes/gas.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"patterns": [
{
"name": "keyword.instruction.assembly",
"match": "\\b(mov|add|sub|mul|imul|div|idiv|call|jmp|je|jne|ja|jb|jbe|jae|jg|jl|jle|jge|jo|jno|js|jns|jz|jnz|ret|push|pop|xchg|lea|cmp|test|and|or|xor|not|shl|shr|sar|sal|rol|ror|inc|dec|neg|nop|int|iret|hlt|cli|sti|lgdt|lldt|ltr|lgs|lfs|lss|leave|enter|loop|loope|loopne|loopz|loopnz|jcxz|jecxz|cpuid|rdmsr|wrmsr)(?:[bwlq])?\\b"
"match": "\\b(mov|add|sub|mul|imul|div|idiv|call|jmp|je|jne|ja|jb|jbe|jae|jg|jl|jle|jge|jo|jno|js|jns|jz|jnz|ret|push|pop|pusha|popa|xchg|lea|cmp|test|and|or|xor|not|shl|shr|sar|sal|rol|ror|inc|dec|neg|nop|int|iret|hlt|cli|sti|lgdt|lidt|ltr|sgdt|sidt|str|lgs|lfs|lss|leave|enter|loop|loope|loopne|loopz|loopnz|jcxz|jecxz|cpuid|rdmsr|wrmsr|rdtsc|rdtscp|cld|std|stc|clc|cmc|pushf|popf|cbw|cwde|cdqe|cwd|cdq|cqo|invlpg|invd|wbinvd)(?:[bwlq])?\\b"
}
]
},
Expand Down
Loading
Loading