neoh 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- main.py +1055 -0
- neoh-0.1.0.dist-info/METADATA +135 -0
- neoh-0.1.0.dist-info/RECORD +7 -0
- neoh-0.1.0.dist-info/WHEEL +5 -0
- neoh-0.1.0.dist-info/entry_points.txt +2 -0
- neoh-0.1.0.dist-info/licenses/LICENSE +201 -0
- neoh-0.1.0.dist-info/top_level.txt +1 -0
main.py
ADDED
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NeoH → SystemVerilog Transpiler (Python Edition)
|
|
4
|
+
================================================
|
|
5
|
+
Rewritten from the Rust/pest original with substantial new features:
|
|
6
|
+
|
|
7
|
+
• Zero-dependency recursive-descent parser with precise error locations
|
|
8
|
+
• Full AST (no more fragile string-hacking)
|
|
9
|
+
• Symbol table & semantic analysis (undefined references, duplicate ports, etc.)
|
|
10
|
+
• Complete grammar coverage: known, piece, block, testbench, testgroup, aff,
|
|
11
|
+
when, expect, pulse, watchfor, writefile, same-blocks, tempassign, ranges
|
|
12
|
+
• Pretty-printed SystemVerilog with configurable indentation
|
|
13
|
+
• Auto-generated testbench boilerplate (clock, reset, VCD dump)
|
|
14
|
+
• CLI with --check, --ast-dump, and --dump-vcd modes
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from dataclasses import dataclass, field, asdict
|
|
21
|
+
from enum import Enum, auto
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import List, Dict, Optional, Union, Tuple, Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# 1. TOKENISER
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
class TokType(Enum):
|
|
31
|
+
# keywords
|
|
32
|
+
KNOWN = auto(); PIECE = auto(); BLOCK = auto(); PIED = auto()
|
|
33
|
+
IN = auto(); OUT = auto(); VAR = auto(); RET = auto()
|
|
34
|
+
TEMPASSIGN = auto(); ALWAYS = auto(); PASSPARAMS = auto()
|
|
35
|
+
TESTBENCH = auto(); TARGET = auto(); GETVARS = auto(); WHEN = auto()
|
|
36
|
+
PUT = auto(); EXPECT = auto(); PULSE = auto(); LEN = auto(); GAP = auto()
|
|
37
|
+
WATCHFOR = auto(); WRITEFILE = auto(); MODE = auto(); FILE = auto()
|
|
38
|
+
TESTGROUP = auto(); DO = auto(); SAME = auto(); AFF = auto()
|
|
39
|
+
POSEDGE = auto(); NEGEDGE = auto(); OR = auto()
|
|
40
|
+
# operators / punctuation
|
|
41
|
+
LE = auto(); EQ = auto(); ASSIGN = auto(); PLUS = auto(); MINUS = auto()
|
|
42
|
+
AND = auto(); OR_OP = auto(); BANG = auto(); STAR = auto()
|
|
43
|
+
LPAREN = auto(); RPAREN = auto(); LBRACE = auto(); RBRACE = auto()
|
|
44
|
+
LBRACKET = auto(); RBRACKET = auto(); SEMICOLON = auto(); COLON = auto()
|
|
45
|
+
COMMA = auto(); DOT = auto(); SLASH = auto()
|
|
46
|
+
# literals
|
|
47
|
+
IDENT = auto(); INT = auto(); STRING = auto()
|
|
48
|
+
EOF = auto()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
KEYWORDS: Dict[str, TokType] = {
|
|
52
|
+
"known": TokType.KNOWN, "piece": TokType.PIECE, "block": TokType.BLOCK,
|
|
53
|
+
"pieced": TokType.PIED, "in": TokType.IN, "out": TokType.OUT,
|
|
54
|
+
"var": TokType.VAR, "ret": TokType.RET, "tempassign": TokType.TEMPASSIGN,
|
|
55
|
+
"always": TokType.ALWAYS, "passparams": TokType.PASSPARAMS,
|
|
56
|
+
"testbench": TokType.TESTBENCH, "target": TokType.TARGET,
|
|
57
|
+
"getvars": TokType.GETVARS, "when": TokType.WHEN, "put": TokType.PUT,
|
|
58
|
+
"expect": TokType.EXPECT, "pulse": TokType.PULSE, "len": TokType.LEN,
|
|
59
|
+
"gap": TokType.GAP, "watchfor": TokType.WATCHFOR,
|
|
60
|
+
"writefile": TokType.WRITEFILE, "mode": TokType.MODE, "file": TokType.FILE,
|
|
61
|
+
"testgroup": TokType.TESTGROUP, "do": TokType.DO, "same": TokType.SAME,
|
|
62
|
+
"aff": TokType.AFF, "posedge": TokType.POSEDGE, "negedge": TokType.NEGEDGE,
|
|
63
|
+
"or": TokType.OR,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Token:
|
|
69
|
+
type: TokType
|
|
70
|
+
value: Any
|
|
71
|
+
line: int
|
|
72
|
+
col: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Tokenizer:
|
|
76
|
+
def __init__(self, text: str):
|
|
77
|
+
self.text = text
|
|
78
|
+
self.pos = 0
|
|
79
|
+
self.line = 1
|
|
80
|
+
self.col = 1
|
|
81
|
+
|
|
82
|
+
def peek(self) -> str:
|
|
83
|
+
return "\0" if self.pos >= len(self.text) else self.text[self.pos]
|
|
84
|
+
|
|
85
|
+
def advance(self) -> str:
|
|
86
|
+
ch = self.peek()
|
|
87
|
+
if ch == "\n":
|
|
88
|
+
self.line += 1
|
|
89
|
+
self.col = 1
|
|
90
|
+
else:
|
|
91
|
+
self.col += 1
|
|
92
|
+
self.pos += 1
|
|
93
|
+
return ch
|
|
94
|
+
|
|
95
|
+
def skip_ws_and_comments(self):
|
|
96
|
+
while True:
|
|
97
|
+
# whitespace
|
|
98
|
+
while self.peek() in " \t\r\n":
|
|
99
|
+
self.advance()
|
|
100
|
+
# comments
|
|
101
|
+
if self.peek() == "/" and self.pos + 1 < len(self.text):
|
|
102
|
+
nxt = self.text[self.pos + 1]
|
|
103
|
+
if nxt == "/":
|
|
104
|
+
while self.peek() not in "\n\0":
|
|
105
|
+
self.advance()
|
|
106
|
+
elif nxt == "*":
|
|
107
|
+
self.advance(); self.advance()
|
|
108
|
+
while not (self.peek() == "*" and self.pos + 1 < len(self.text) and self.text[self.pos + 1] == "/"):
|
|
109
|
+
if self.peek() == "\0":
|
|
110
|
+
raise SyntaxError(f"Unterminated block comment at line {self.line}")
|
|
111
|
+
self.advance()
|
|
112
|
+
self.advance(); self.advance()
|
|
113
|
+
else:
|
|
114
|
+
break
|
|
115
|
+
else:
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
def tokenize(self) -> List[Token]:
|
|
119
|
+
tokens: List[Token] = []
|
|
120
|
+
while True:
|
|
121
|
+
self.skip_ws_and_comments()
|
|
122
|
+
sl, sc = self.line, self.col
|
|
123
|
+
ch = self.peek()
|
|
124
|
+
|
|
125
|
+
if ch == "\0":
|
|
126
|
+
tokens.append(Token(TokType.EOF, None, sl, sc))
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
# two-char operators
|
|
130
|
+
if ch == "<" and self.pos + 1 < len(self.text) and self.text[self.pos + 1] == "=":
|
|
131
|
+
self.advance(); self.advance()
|
|
132
|
+
tokens.append(Token(TokType.LE, "<=", sl, sc)); continue
|
|
133
|
+
if ch == "=" and self.pos + 1 < len(self.text) and self.text[self.pos + 1] == "=":
|
|
134
|
+
self.advance(); self.advance()
|
|
135
|
+
tokens.append(Token(TokType.EQ, "==", sl, sc)); continue
|
|
136
|
+
|
|
137
|
+
# single-char punctuation
|
|
138
|
+
single = {
|
|
139
|
+
"=": TokType.ASSIGN, "+": TokType.PLUS, "-": TokType.MINUS,
|
|
140
|
+
"&": TokType.AND, "|": TokType.OR_OP, "!": TokType.BANG,
|
|
141
|
+
"*": TokType.STAR, "(": TokType.LPAREN, ")": TokType.RPAREN,
|
|
142
|
+
"{": TokType.LBRACE, "}": TokType.RBRACE, "[": TokType.LBRACKET,
|
|
143
|
+
"]": TokType.RBRACKET, ";": TokType.SEMICOLON, ":": TokType.COLON,
|
|
144
|
+
",": TokType.COMMA, ".": TokType.DOT, "/": TokType.SLASH,
|
|
145
|
+
}
|
|
146
|
+
if ch in single:
|
|
147
|
+
self.advance()
|
|
148
|
+
tokens.append(Token(single[ch], ch, sl, sc)); continue
|
|
149
|
+
|
|
150
|
+
# string literal
|
|
151
|
+
if ch == '"':
|
|
152
|
+
self.advance()
|
|
153
|
+
parts = []
|
|
154
|
+
while self.peek() not in '"\0':
|
|
155
|
+
if self.peek() == "\\":
|
|
156
|
+
self.advance()
|
|
157
|
+
esc = self.advance()
|
|
158
|
+
parts.append({"n": "\n", "t": "\t", '"': '"', "\\": "\\"}.get(esc, esc))
|
|
159
|
+
else:
|
|
160
|
+
parts.append(self.advance())
|
|
161
|
+
if self.peek() != '"':
|
|
162
|
+
raise SyntaxError(f"Unterminated string at line {sl}")
|
|
163
|
+
self.advance()
|
|
164
|
+
tokens.append(Token(TokType.STRING, '"' + "".join(parts) + '"', sl, sc)); continue
|
|
165
|
+
|
|
166
|
+
# integer
|
|
167
|
+
if ch.isdigit():
|
|
168
|
+
num = []
|
|
169
|
+
while self.peek().isdigit():
|
|
170
|
+
num.append(self.advance())
|
|
171
|
+
tokens.append(Token(TokType.INT, int("".join(num)), sl, sc)); continue
|
|
172
|
+
|
|
173
|
+
# identifier / keyword
|
|
174
|
+
if ch.isalpha() or ch == "_":
|
|
175
|
+
ident = []
|
|
176
|
+
while self.peek().isalnum() or self.peek() == "_":
|
|
177
|
+
ident.append(self.advance())
|
|
178
|
+
word = "".join(ident)
|
|
179
|
+
tokens.append(Token(KEYWORDS.get(word, TokType.IDENT), word, sl, sc)); continue
|
|
180
|
+
|
|
181
|
+
raise SyntaxError(f"Unexpected character {ch!r} at line {sl}, col {sc}")
|
|
182
|
+
|
|
183
|
+
return tokens
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# =============================================================================
|
|
187
|
+
# 2. AST NODES
|
|
188
|
+
# =============================================================================
|
|
189
|
+
|
|
190
|
+
@dataclass
|
|
191
|
+
class ExprOp:
|
|
192
|
+
value: Union[str, int]
|
|
193
|
+
kind: str # "string" | "ident" | "int"
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class Expr:
|
|
197
|
+
ops: List[ExprOp]
|
|
198
|
+
binops: List[str]
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class KnownStmt:
|
|
202
|
+
name: str
|
|
203
|
+
values: List[str]
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class PiecePort:
|
|
207
|
+
direction: str
|
|
208
|
+
name: str
|
|
209
|
+
|
|
210
|
+
@dataclass
|
|
211
|
+
class PieceDef:
|
|
212
|
+
name: str
|
|
213
|
+
ports: List[PiecePort]
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class Port:
|
|
217
|
+
direction: str
|
|
218
|
+
name: str
|
|
219
|
+
|
|
220
|
+
@dataclass
|
|
221
|
+
class PieceBind:
|
|
222
|
+
instance: str
|
|
223
|
+
interface: str
|
|
224
|
+
|
|
225
|
+
@dataclass
|
|
226
|
+
class VarDecl:
|
|
227
|
+
name: str
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class AssignStmt:
|
|
231
|
+
target: str
|
|
232
|
+
expr: Expr
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class PassParams:
|
|
236
|
+
instance: str
|
|
237
|
+
module: str
|
|
238
|
+
params: List[str]
|
|
239
|
+
|
|
240
|
+
@dataclass
|
|
241
|
+
class RetStmt:
|
|
242
|
+
tempassign: bool
|
|
243
|
+
name: str
|
|
244
|
+
range: Optional[Tuple[int, int]]
|
|
245
|
+
expr: Expr
|
|
246
|
+
|
|
247
|
+
@dataclass
|
|
248
|
+
class GetVarItem:
|
|
249
|
+
invert: bool
|
|
250
|
+
name: str # "*" allowed
|
|
251
|
+
|
|
252
|
+
@dataclass
|
|
253
|
+
class GetVarsCmd:
|
|
254
|
+
items: List[GetVarItem]
|
|
255
|
+
|
|
256
|
+
@dataclass
|
|
257
|
+
class WhenCmd:
|
|
258
|
+
condition: str
|
|
259
|
+
commands: List["VerifCmd"]
|
|
260
|
+
|
|
261
|
+
@dataclass
|
|
262
|
+
class OutCmd:
|
|
263
|
+
delay: int
|
|
264
|
+
value: ExprOp # string or ident
|
|
265
|
+
|
|
266
|
+
@dataclass
|
|
267
|
+
class PutCmd:
|
|
268
|
+
target: str
|
|
269
|
+
expr: Expr
|
|
270
|
+
|
|
271
|
+
@dataclass
|
|
272
|
+
class ExpectCmd:
|
|
273
|
+
delay: int
|
|
274
|
+
expr: Expr
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class PulseCmd:
|
|
278
|
+
len_sig: str
|
|
279
|
+
gap_sig: str
|
|
280
|
+
|
|
281
|
+
@dataclass
|
|
282
|
+
class WatchCmd:
|
|
283
|
+
delay: int
|
|
284
|
+
target: str
|
|
285
|
+
source: str
|
|
286
|
+
out: OutCmd
|
|
287
|
+
|
|
288
|
+
@dataclass
|
|
289
|
+
class WriteFileCmd:
|
|
290
|
+
mode: str
|
|
291
|
+
file_var: str
|
|
292
|
+
file_ext: str
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class DoCmd:
|
|
296
|
+
name: str
|
|
297
|
+
|
|
298
|
+
@dataclass
|
|
299
|
+
class SameBlock:
|
|
300
|
+
names: List[str]
|
|
301
|
+
|
|
302
|
+
@dataclass
|
|
303
|
+
class TestGroup:
|
|
304
|
+
name: str
|
|
305
|
+
items: List[Union[DoCmd, SameBlock]]
|
|
306
|
+
|
|
307
|
+
@dataclass
|
|
308
|
+
class EdgeTerm:
|
|
309
|
+
edge: str
|
|
310
|
+
signal: str
|
|
311
|
+
|
|
312
|
+
@dataclass
|
|
313
|
+
class EdgeExpr:
|
|
314
|
+
terms: List[EdgeTerm]
|
|
315
|
+
|
|
316
|
+
@dataclass
|
|
317
|
+
class AffStmt:
|
|
318
|
+
edge_expr: EdgeExpr
|
|
319
|
+
stmts: List[Union[VarDecl, AssignStmt, RetStmt, PassParams]]
|
|
320
|
+
|
|
321
|
+
@dataclass
|
|
322
|
+
class BlockDef:
|
|
323
|
+
pieced: bool
|
|
324
|
+
name: str
|
|
325
|
+
ports: List[Union[Port, PieceBind]]
|
|
326
|
+
instance: Optional[str]
|
|
327
|
+
stmts: List[Union[VarDecl, AssignStmt, RetStmt, PassParams]]
|
|
328
|
+
|
|
329
|
+
@dataclass
|
|
330
|
+
class TestBench:
|
|
331
|
+
name: str
|
|
332
|
+
target: str
|
|
333
|
+
stmts: List[Union[VarDecl, AssignStmt, RetStmt, PassParams, "VerifCmd"]]
|
|
334
|
+
|
|
335
|
+
VerifCmd = Union[GetVarsCmd, WhenCmd, OutCmd, PutCmd, ExpectCmd, PulseCmd, WatchCmd, WriteFileCmd]
|
|
336
|
+
Stmt = Union[KnownStmt, PieceDef, BlockDef, TestBench, TestGroup, AffStmt]
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# =============================================================================
|
|
340
|
+
# 3. PARSER (recursive descent)
|
|
341
|
+
# =============================================================================
|
|
342
|
+
|
|
343
|
+
class Parser:
|
|
344
|
+
def __init__(self, tokens: List[Token]):
|
|
345
|
+
self.tokens = tokens
|
|
346
|
+
self.pos = 0
|
|
347
|
+
|
|
348
|
+
def cur(self) -> Token:
|
|
349
|
+
return self.tokens[min(self.pos, len(self.tokens) - 1)]
|
|
350
|
+
|
|
351
|
+
def adv(self) -> Token:
|
|
352
|
+
t = self.cur()
|
|
353
|
+
if self.pos < len(self.tokens) - 1:
|
|
354
|
+
self.pos += 1
|
|
355
|
+
return t
|
|
356
|
+
|
|
357
|
+
def expect(self, *types: TokType) -> Token:
|
|
358
|
+
t = self.cur()
|
|
359
|
+
if t.type not in types:
|
|
360
|
+
raise SyntaxError(
|
|
361
|
+
f"Expected {' | '.join(tt.name for tt in types)}, got {t.type.name} "
|
|
362
|
+
f"at line {t.line}, col {t.col} (near {t.value!r})"
|
|
363
|
+
)
|
|
364
|
+
return self.adv()
|
|
365
|
+
|
|
366
|
+
def match(self, *types: TokType) -> bool:
|
|
367
|
+
return self.cur().type in types
|
|
368
|
+
|
|
369
|
+
def opt(self, t: TokType) -> Optional[Token]:
|
|
370
|
+
return self.adv() if self.match(t) else None
|
|
371
|
+
|
|
372
|
+
def parse(self) -> List[Stmt]:
|
|
373
|
+
stmts: List[Stmt] = []
|
|
374
|
+
while not self.match(TokType.EOF):
|
|
375
|
+
stmts.append(self.parse_stmt())
|
|
376
|
+
return stmts
|
|
377
|
+
|
|
378
|
+
def parse_stmt(self) -> Stmt:
|
|
379
|
+
if self.match(TokType.KNOWN): return self.parse_known()
|
|
380
|
+
if self.match(TokType.PIECE): return self.parse_piece()
|
|
381
|
+
if self.match(TokType.BLOCK, TokType.PIED): return self.parse_block()
|
|
382
|
+
if self.match(TokType.TESTBENCH): return self.parse_testbench()
|
|
383
|
+
if self.match(TokType.TESTGROUP): return self.parse_testgroup()
|
|
384
|
+
if self.match(TokType.AFF): return self.parse_aff()
|
|
385
|
+
raise SyntaxError(f"Unexpected {self.cur().type.name} at line {self.cur().line}")
|
|
386
|
+
|
|
387
|
+
def parse_known(self) -> KnownStmt:
|
|
388
|
+
self.expect(TokType.KNOWN)
|
|
389
|
+
name = self.expect(TokType.IDENT).value
|
|
390
|
+
self.expect(TokType.LE)
|
|
391
|
+
vals = [self.expect(TokType.IDENT).value]
|
|
392
|
+
while self.opt(TokType.COMMA):
|
|
393
|
+
vals.append(self.expect(TokType.IDENT).value)
|
|
394
|
+
self.opt(TokType.SEMICOLON)
|
|
395
|
+
return KnownStmt(name, vals)
|
|
396
|
+
|
|
397
|
+
def parse_piece(self) -> PieceDef:
|
|
398
|
+
self.expect(TokType.PIECE)
|
|
399
|
+
name = self.expect(TokType.IDENT).value
|
|
400
|
+
self.expect(TokType.LBRACE)
|
|
401
|
+
ports: List[PiecePort] = []
|
|
402
|
+
while self.match(TokType.IN, TokType.OUT):
|
|
403
|
+
d = self.adv().value
|
|
404
|
+
ports.append(PiecePort(d, self.expect(TokType.IDENT).value))
|
|
405
|
+
self.opt(TokType.COMMA)
|
|
406
|
+
self.expect(TokType.RBRACE)
|
|
407
|
+
return PieceDef(name, ports)
|
|
408
|
+
|
|
409
|
+
def parse_block(self) -> BlockDef:
|
|
410
|
+
pieced = self.opt(TokType.PIED) is not None
|
|
411
|
+
self.expect(TokType.BLOCK)
|
|
412
|
+
name = self.expect(TokType.IDENT).value
|
|
413
|
+
self.expect(TokType.LPAREN)
|
|
414
|
+
ports: List[Union[Port, PieceBind]] = []
|
|
415
|
+
if not self.match(TokType.RPAREN):
|
|
416
|
+
ports = self.parse_port_list()
|
|
417
|
+
self.expect(TokType.RPAREN)
|
|
418
|
+
inst = self.expect(TokType.IDENT).value if self.match(TokType.IDENT) else None
|
|
419
|
+
self.expect(TokType.LBRACE)
|
|
420
|
+
stmts: List[Union[VarDecl, AssignStmt, RetStmt, PassParams]] = []
|
|
421
|
+
while not self.match(TokType.RBRACE):
|
|
422
|
+
stmts.append(self.parse_block_stmt())
|
|
423
|
+
self.expect(TokType.RBRACE)
|
|
424
|
+
return BlockDef(pieced, name, ports, inst, stmts)
|
|
425
|
+
|
|
426
|
+
def parse_port_list(self) -> List[Union[Port, PieceBind]]:
|
|
427
|
+
ports: List[Union[Port, PieceBind]] = []
|
|
428
|
+
while True:
|
|
429
|
+
if self.match(TokType.IN, TokType.OUT):
|
|
430
|
+
ports.append(Port(self.adv().value, self.expect(TokType.IDENT).value))
|
|
431
|
+
elif self.match(TokType.IDENT):
|
|
432
|
+
inst = self.adv().value
|
|
433
|
+
self.expect(TokType.LE)
|
|
434
|
+
ports.append(PieceBind(inst, self.expect(TokType.IDENT).value))
|
|
435
|
+
else:
|
|
436
|
+
break
|
|
437
|
+
if not self.opt(TokType.COMMA):
|
|
438
|
+
break
|
|
439
|
+
return ports
|
|
440
|
+
|
|
441
|
+
def parse_block_stmt(self) -> Union[VarDecl, AssignStmt, RetStmt, PassParams]:
|
|
442
|
+
if self.match(TokType.VAR):
|
|
443
|
+
self.expect(TokType.VAR)
|
|
444
|
+
v = VarDecl(self.expect(TokType.IDENT).value)
|
|
445
|
+
self.expect(TokType.SEMICOLON); return v
|
|
446
|
+
if self.match(TokType.RET):
|
|
447
|
+
return self.parse_ret()
|
|
448
|
+
# assign vs passparams → look-ahead
|
|
449
|
+
if self.match(TokType.IDENT) and self.pos + 1 < len(self.tokens):
|
|
450
|
+
nxt = self.tokens[self.pos + 1].type
|
|
451
|
+
if nxt == TokType.PASSPARAMS:
|
|
452
|
+
return self.parse_passparams()
|
|
453
|
+
return self.parse_assign()
|
|
454
|
+
|
|
455
|
+
def parse_assign(self) -> AssignStmt:
|
|
456
|
+
tgt = self.expect(TokType.IDENT).value
|
|
457
|
+
self.expect(TokType.ASSIGN)
|
|
458
|
+
e = self.parse_expr()
|
|
459
|
+
self.expect(TokType.SEMICOLON)
|
|
460
|
+
return AssignStmt(tgt, e)
|
|
461
|
+
|
|
462
|
+
def parse_passparams(self) -> PassParams:
|
|
463
|
+
inst = self.expect(TokType.IDENT).value
|
|
464
|
+
self.expect(TokType.PASSPARAMS)
|
|
465
|
+
mod = self.expect(TokType.IDENT).value
|
|
466
|
+
self.expect(TokType.LPAREN)
|
|
467
|
+
ps = [self.expect(TokType.IDENT).value]
|
|
468
|
+
while self.opt(TokType.COMMA):
|
|
469
|
+
ps.append(self.expect(TokType.IDENT).value)
|
|
470
|
+
self.expect(TokType.RPAREN)
|
|
471
|
+
self.expect(TokType.SEMICOLON)
|
|
472
|
+
return PassParams(inst, mod, ps)
|
|
473
|
+
|
|
474
|
+
def parse_ret(self) -> RetStmt:
|
|
475
|
+
self.expect(TokType.RET)
|
|
476
|
+
tmp = self.opt(TokType.TEMPASSIGN) is not None
|
|
477
|
+
name = self.expect(TokType.IDENT).value
|
|
478
|
+
rng: Optional[Tuple[int, int]] = None
|
|
479
|
+
if self.opt(TokType.LBRACKET):
|
|
480
|
+
self.expect(TokType.ALWAYS)
|
|
481
|
+
a = self.expect(TokType.INT).value
|
|
482
|
+
self.expect(TokType.COLON)
|
|
483
|
+
b = self.expect(TokType.INT).value
|
|
484
|
+
self.expect(TokType.RBRACKET)
|
|
485
|
+
rng = (a, b)
|
|
486
|
+
self.expect(TokType.LE)
|
|
487
|
+
e = self.parse_expr()
|
|
488
|
+
self.expect(TokType.SEMICOLON)
|
|
489
|
+
return RetStmt(tmp, name, rng, e)
|
|
490
|
+
|
|
491
|
+
def parse_expr(self) -> Expr:
|
|
492
|
+
ops = [self.parse_expr_op()]
|
|
493
|
+
bins: List[str] = []
|
|
494
|
+
while self.match(TokType.PLUS, TokType.MINUS, TokType.AND, TokType.OR_OP, TokType.EQ, TokType.LE):
|
|
495
|
+
bins.append(self.adv().value)
|
|
496
|
+
ops.append(self.parse_expr_op())
|
|
497
|
+
return Expr(ops, bins)
|
|
498
|
+
|
|
499
|
+
def parse_expr_op(self) -> ExprOp:
|
|
500
|
+
t = self.cur()
|
|
501
|
+
if t.type == TokType.STRING:
|
|
502
|
+
self.adv(); return ExprOp(t.value, "string")
|
|
503
|
+
if t.type == TokType.IDENT:
|
|
504
|
+
self.adv(); return ExprOp(t.value, "ident")
|
|
505
|
+
if t.type == TokType.INT:
|
|
506
|
+
self.adv(); return ExprOp(t.value, "int")
|
|
507
|
+
raise SyntaxError(f"Expected expression operand, got {t.type.name} at line {t.line}")
|
|
508
|
+
|
|
509
|
+
def parse_testbench(self) -> TestBench:
|
|
510
|
+
self.expect(TokType.TESTBENCH)
|
|
511
|
+
name = self.expect(TokType.IDENT).value
|
|
512
|
+
self.expect(TokType.TARGET); self.expect(TokType.LPAREN)
|
|
513
|
+
tgt = self.expect(TokType.IDENT).value
|
|
514
|
+
self.expect(TokType.RPAREN); self.expect(TokType.LBRACE)
|
|
515
|
+
stmts: List[Union[VarDecl, AssignStmt, RetStmt, PassParams, VerifCmd]] = []
|
|
516
|
+
while not self.match(TokType.RBRACE):
|
|
517
|
+
if self.match(TokType.VAR, TokType.RET, TokType.IDENT):
|
|
518
|
+
stmts.append(self.parse_block_stmt())
|
|
519
|
+
else:
|
|
520
|
+
stmts.append(self.parse_verif())
|
|
521
|
+
self.expect(TokType.RBRACE)
|
|
522
|
+
return TestBench(name, tgt, stmts)
|
|
523
|
+
|
|
524
|
+
def parse_verif(self) -> VerifCmd:
|
|
525
|
+
if self.match(TokType.GETVARS): return self.parse_getvars()
|
|
526
|
+
if self.match(TokType.WHEN): return self.parse_when()
|
|
527
|
+
if self.match(TokType.SLASH): return self.parse_slash_cmd()
|
|
528
|
+
if self.match(TokType.PUT): return self.parse_put()
|
|
529
|
+
if self.match(TokType.PULSE): return self.parse_pulse()
|
|
530
|
+
if self.match(TokType.WRITEFILE): return self.parse_writefile()
|
|
531
|
+
raise SyntaxError(f"Unexpected verification command {self.cur().type.name} at line {self.cur().line}")
|
|
532
|
+
|
|
533
|
+
def parse_getvars(self) -> GetVarsCmd:
|
|
534
|
+
self.expect(TokType.GETVARS); self.expect(TokType.LPAREN)
|
|
535
|
+
items = [self.parse_getvar_item()]
|
|
536
|
+
while self.opt(TokType.COMMA):
|
|
537
|
+
items.append(self.parse_getvar_item())
|
|
538
|
+
self.expect(TokType.RPAREN); self.expect(TokType.SEMICOLON)
|
|
539
|
+
return GetVarsCmd(items)
|
|
540
|
+
|
|
541
|
+
def parse_getvar_item(self) -> GetVarItem:
|
|
542
|
+
inv = self.opt(TokType.BANG) is not None
|
|
543
|
+
if self.match(TokType.IDENT):
|
|
544
|
+
return GetVarItem(inv, self.adv().value)
|
|
545
|
+
if self.match(TokType.STAR):
|
|
546
|
+
self.adv(); return GetVarItem(inv, "*")
|
|
547
|
+
raise SyntaxError(f"Expected identifier or * in getvars at line {self.cur().line}")
|
|
548
|
+
|
|
549
|
+
def parse_when(self) -> WhenCmd:
|
|
550
|
+
self.expect(TokType.WHEN); self.expect(TokType.LPAREN)
|
|
551
|
+
cond = self.expect(TokType.IDENT).value
|
|
552
|
+
self.expect(TokType.RPAREN); self.expect(TokType.LBRACE)
|
|
553
|
+
cmds: List[VerifCmd] = []
|
|
554
|
+
while not self.match(TokType.RBRACE):
|
|
555
|
+
cmds.append(self.parse_verif())
|
|
556
|
+
self.expect(TokType.RBRACE)
|
|
557
|
+
return WhenCmd(cond, cmds)
|
|
558
|
+
|
|
559
|
+
def parse_slash_cmd(self) -> Union[OutCmd, ExpectCmd, WatchCmd]:
|
|
560
|
+
self.expect(TokType.SLASH)
|
|
561
|
+
delay = self.expect(TokType.INT).value
|
|
562
|
+
if self.match(TokType.OUT):
|
|
563
|
+
return self.parse_out(delay)
|
|
564
|
+
if self.match(TokType.EXPECT):
|
|
565
|
+
return self.parse_expect(delay)
|
|
566
|
+
if self.match(TokType.WATCHFOR):
|
|
567
|
+
return self.parse_watch(delay)
|
|
568
|
+
raise SyntaxError(f"Unknown / command at line {self.cur().line}")
|
|
569
|
+
|
|
570
|
+
def parse_out(self, delay: int) -> OutCmd:
|
|
571
|
+
self.expect(TokType.OUT); self.expect(TokType.LPAREN)
|
|
572
|
+
if self.match(TokType.STRING):
|
|
573
|
+
v = ExprOp(self.adv().value, "string")
|
|
574
|
+
else:
|
|
575
|
+
v = ExprOp(self.expect(TokType.IDENT).value, "ident")
|
|
576
|
+
self.expect(TokType.RPAREN); self.opt(TokType.SEMICOLON)
|
|
577
|
+
return OutCmd(delay, v)
|
|
578
|
+
|
|
579
|
+
def parse_expect(self, delay: int) -> ExpectCmd:
|
|
580
|
+
self.expect(TokType.EXPECT); self.expect(TokType.LPAREN)
|
|
581
|
+
e = self.parse_expr()
|
|
582
|
+
self.expect(TokType.RPAREN); self.expect(TokType.SEMICOLON)
|
|
583
|
+
return ExpectCmd(delay, e)
|
|
584
|
+
|
|
585
|
+
def parse_watch(self, delay: int) -> WatchCmd:
|
|
586
|
+
self.expect(TokType.WATCHFOR)
|
|
587
|
+
tgt = self.expect(TokType.IDENT).value
|
|
588
|
+
self.expect(TokType.LE)
|
|
589
|
+
src = self.expect(TokType.IDENT).value
|
|
590
|
+
self.expect(TokType.AND)
|
|
591
|
+
self.expect(TokType.SLASH)
|
|
592
|
+
out_delay = self.expect(TokType.INT).value
|
|
593
|
+
out = self.parse_out(out_delay)
|
|
594
|
+
return WatchCmd(delay, tgt, src, out)
|
|
595
|
+
|
|
596
|
+
def parse_put(self) -> PutCmd:
|
|
597
|
+
self.expect(TokType.PUT)
|
|
598
|
+
tgt = self.expect(TokType.IDENT).value
|
|
599
|
+
self.expect(TokType.LE)
|
|
600
|
+
e = self.parse_expr()
|
|
601
|
+
self.expect(TokType.SEMICOLON)
|
|
602
|
+
return PutCmd(tgt, e)
|
|
603
|
+
|
|
604
|
+
def parse_pulse(self) -> PulseCmd:
|
|
605
|
+
self.expect(TokType.PULSE); self.expect(TokType.LEN); self.expect(TokType.LPAREN)
|
|
606
|
+
l = self.expect(TokType.IDENT).value
|
|
607
|
+
self.expect(TokType.RPAREN); self.expect(TokType.COMMA)
|
|
608
|
+
self.expect(TokType.GAP); self.expect(TokType.LPAREN)
|
|
609
|
+
g = self.expect(TokType.IDENT).value
|
|
610
|
+
self.expect(TokType.RPAREN); self.expect(TokType.SEMICOLON)
|
|
611
|
+
return PulseCmd(l, g)
|
|
612
|
+
|
|
613
|
+
def parse_writefile(self) -> WriteFileCmd:
|
|
614
|
+
self.expect(TokType.WRITEFILE); self.expect(TokType.LPAREN)
|
|
615
|
+
self.expect(TokType.MODE)
|
|
616
|
+
mode = self.expect(TokType.IDENT).value
|
|
617
|
+
self.expect(TokType.COMMA); self.expect(TokType.FILE)
|
|
618
|
+
fvar = self.expect(TokType.IDENT).value
|
|
619
|
+
self.expect(TokType.DOT)
|
|
620
|
+
fext = self.expect(TokType.IDENT).value
|
|
621
|
+
self.expect(TokType.RPAREN); self.expect(TokType.SEMICOLON)
|
|
622
|
+
return WriteFileCmd(mode, fvar, fext)
|
|
623
|
+
|
|
624
|
+
def parse_testgroup(self) -> TestGroup:
|
|
625
|
+
self.expect(TokType.TESTGROUP)
|
|
626
|
+
name = self.expect(TokType.IDENT).value
|
|
627
|
+
self.expect(TokType.LBRACE)
|
|
628
|
+
items: List[Union[DoCmd, SameBlock]] = []
|
|
629
|
+
while not self.match(TokType.RBRACE):
|
|
630
|
+
if self.match(TokType.DO):
|
|
631
|
+
self.adv()
|
|
632
|
+
items.append(DoCmd(self.expect(TokType.IDENT).value))
|
|
633
|
+
self.expect(TokType.SEMICOLON)
|
|
634
|
+
elif self.match(TokType.SAME):
|
|
635
|
+
self.adv(); self.expect(TokType.LBRACE)
|
|
636
|
+
names: List[str] = []
|
|
637
|
+
while not self.match(TokType.RBRACE):
|
|
638
|
+
names.append(self.expect(TokType.IDENT).value)
|
|
639
|
+
self.expect(TokType.RBRACE)
|
|
640
|
+
items.append(SameBlock(names))
|
|
641
|
+
else:
|
|
642
|
+
raise SyntaxError(f"Unexpected {self.cur().type.name} in testgroup at line {self.cur().line}")
|
|
643
|
+
self.expect(TokType.RBRACE)
|
|
644
|
+
return TestGroup(name, items)
|
|
645
|
+
|
|
646
|
+
def parse_aff(self) -> AffStmt:
|
|
647
|
+
self.expect(TokType.AFF)
|
|
648
|
+
ee = self.parse_edge_expr()
|
|
649
|
+
self.expect(TokType.LBRACE)
|
|
650
|
+
stmts: List[Union[VarDecl, AssignStmt, RetStmt, PassParams]] = []
|
|
651
|
+
while not self.match(TokType.RBRACE):
|
|
652
|
+
stmts.append(self.parse_block_stmt())
|
|
653
|
+
self.expect(TokType.RBRACE)
|
|
654
|
+
return AffStmt(ee, stmts)
|
|
655
|
+
|
|
656
|
+
def parse_edge_expr(self) -> EdgeExpr:
|
|
657
|
+
terms = [self.parse_edge_term()]
|
|
658
|
+
while self.opt(TokType.OR):
|
|
659
|
+
terms.append(self.parse_edge_term())
|
|
660
|
+
return EdgeExpr(terms)
|
|
661
|
+
|
|
662
|
+
def parse_edge_term(self) -> EdgeTerm:
|
|
663
|
+
edge = self.expect(TokType.POSEDGE, TokType.NEGEDGE).value
|
|
664
|
+
self.expect(TokType.LPAREN)
|
|
665
|
+
sig = self.expect(TokType.IDENT).value
|
|
666
|
+
self.expect(TokType.RPAREN)
|
|
667
|
+
return EdgeTerm(edge, sig)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
# =============================================================================
|
|
671
|
+
# 4. SYMBOL TABLE & SEMANTIC ANALYSIS
|
|
672
|
+
# =============================================================================
|
|
673
|
+
|
|
674
|
+
class SymbolTable:
|
|
675
|
+
def __init__(self):
|
|
676
|
+
self.scopes: List[Dict[str, Any]] = [{}]
|
|
677
|
+
|
|
678
|
+
def push(self): self.scopes.append({})
|
|
679
|
+
def pop(self): self.scopes.pop()
|
|
680
|
+
|
|
681
|
+
def define(self, name: str, info: Any):
|
|
682
|
+
self.scopes[-1][name] = info
|
|
683
|
+
|
|
684
|
+
def lookup(self, name: str) -> Optional[Any]:
|
|
685
|
+
for sc in reversed(self.scopes):
|
|
686
|
+
if name in sc:
|
|
687
|
+
return sc[name]
|
|
688
|
+
return None
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class SemanticAnalyzer:
|
|
692
|
+
def __init__(self):
|
|
693
|
+
self.errors: List[str] = []
|
|
694
|
+
self.warnings: List[str] = []
|
|
695
|
+
self.sym = SymbolTable()
|
|
696
|
+
|
|
697
|
+
def _err(self, msg: str):
|
|
698
|
+
self.errors.append(msg)
|
|
699
|
+
|
|
700
|
+
def _warn(self, msg: str):
|
|
701
|
+
self.warnings.append(msg)
|
|
702
|
+
|
|
703
|
+
def analyze(self, ast: List[Stmt]) -> bool:
|
|
704
|
+
# first pass: collect top-level names
|
|
705
|
+
for stmt in ast:
|
|
706
|
+
if isinstance(stmt, PieceDef):
|
|
707
|
+
self.sym.define(stmt.name, stmt)
|
|
708
|
+
elif isinstance(stmt, BlockDef):
|
|
709
|
+
self.sym.define(stmt.name, stmt)
|
|
710
|
+
elif isinstance(stmt, TestBench):
|
|
711
|
+
self.sym.define(stmt.name, stmt)
|
|
712
|
+
elif isinstance(stmt, TestGroup):
|
|
713
|
+
self.sym.define(stmt.name, stmt)
|
|
714
|
+
# second pass: detailed checks
|
|
715
|
+
for stmt in ast:
|
|
716
|
+
self._check_stmt(stmt)
|
|
717
|
+
return len(self.errors) == 0
|
|
718
|
+
|
|
719
|
+
def _check_stmt(self, stmt: Stmt):
|
|
720
|
+
if isinstance(stmt, BlockDef):
|
|
721
|
+
self.sym.push()
|
|
722
|
+
for p in stmt.ports:
|
|
723
|
+
if isinstance(p, Port):
|
|
724
|
+
self.sym.define(p.name, p)
|
|
725
|
+
for s in stmt.stmts:
|
|
726
|
+
self._check_block_stmt(s)
|
|
727
|
+
self.sym.pop()
|
|
728
|
+
elif isinstance(stmt, TestBench):
|
|
729
|
+
if not self.sym.lookup(stmt.target):
|
|
730
|
+
self._warn(f"Testbench target '{stmt.target}' not defined")
|
|
731
|
+
for s in stmt.stmts:
|
|
732
|
+
if isinstance(s, (VarDecl, AssignStmt, RetStmt, PassParams)):
|
|
733
|
+
self._check_block_stmt(s)
|
|
734
|
+
else:
|
|
735
|
+
self._check_verif(s)
|
|
736
|
+
elif isinstance(stmt, AffStmt):
|
|
737
|
+
for s in stmt.stmts:
|
|
738
|
+
self._check_block_stmt(s)
|
|
739
|
+
|
|
740
|
+
def _check_block_stmt(self, s: Union[VarDecl, AssignStmt, RetStmt, PassParams]):
|
|
741
|
+
if isinstance(s, VarDecl):
|
|
742
|
+
if self.sym.lookup(s.name):
|
|
743
|
+
self._warn(f"Variable '{s.name}' shadows previous declaration")
|
|
744
|
+
self.sym.define(s.name, s)
|
|
745
|
+
elif isinstance(s, AssignStmt):
|
|
746
|
+
if not self.sym.lookup(s.target):
|
|
747
|
+
self._warn(f"Assignment to undefined variable '{s.target}'")
|
|
748
|
+
self._check_expr(s.expr)
|
|
749
|
+
elif isinstance(s, RetStmt):
|
|
750
|
+
if not self.sym.lookup(s.name):
|
|
751
|
+
self.sym.define(s.name, s)
|
|
752
|
+
self._check_expr(s.expr)
|
|
753
|
+
elif isinstance(s, PassParams):
|
|
754
|
+
if not self.sym.lookup(s.module):
|
|
755
|
+
self._warn(f"Module '{s.module}' not defined for passparams")
|
|
756
|
+
|
|
757
|
+
def _check_expr(self, e: Expr):
|
|
758
|
+
for op in e.ops:
|
|
759
|
+
if op.kind == "ident" and not self.sym.lookup(op.value):
|
|
760
|
+
self._warn(f"Undefined identifier '{op.value}' in expression")
|
|
761
|
+
|
|
762
|
+
def _check_verif(self, v: VerifCmd):
|
|
763
|
+
if isinstance(v, PutCmd):
|
|
764
|
+
if not self.sym.lookup(v.target):
|
|
765
|
+
self._warn(f"Put target '{v.target}' undefined")
|
|
766
|
+
self._check_expr(v.expr)
|
|
767
|
+
elif isinstance(v, ExpectCmd):
|
|
768
|
+
self._check_expr(v.expr)
|
|
769
|
+
elif isinstance(v, WhenCmd):
|
|
770
|
+
for c in v.commands:
|
|
771
|
+
self._check_verif(c)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
# =============================================================================
|
|
775
|
+
# 5. SYSTEMVERILOG CODE GENERATOR
|
|
776
|
+
# =============================================================================
|
|
777
|
+
|
|
778
|
+
class SVGenerator:
|
|
779
|
+
def __init__(self, indent: int = 2, dump_vcd: bool = False):
|
|
780
|
+
self.indent = indent
|
|
781
|
+
self.dump_vcd = dump_vcd
|
|
782
|
+
self.level = 0
|
|
783
|
+
self.out: List[str] = []
|
|
784
|
+
|
|
785
|
+
def _line(self, txt: str = ""):
|
|
786
|
+
self.out.append(" " * (self.level * self.indent) + txt)
|
|
787
|
+
|
|
788
|
+
def _push(self): self.level += 1
|
|
789
|
+
def _pop(self): self.level -= 1
|
|
790
|
+
|
|
791
|
+
def generate(self, ast: List[Stmt]) -> str:
|
|
792
|
+
self._line("`timescale 1ns / 1ps")
|
|
793
|
+
self._line("`default_nettype none")
|
|
794
|
+
self._line("")
|
|
795
|
+
for stmt in ast:
|
|
796
|
+
self._gen_stmt(stmt)
|
|
797
|
+
return "\n".join(self.out)
|
|
798
|
+
|
|
799
|
+
def _gen_stmt(self, stmt: Stmt):
|
|
800
|
+
if isinstance(stmt, KnownStmt): self._gen_known(stmt)
|
|
801
|
+
elif isinstance(stmt, PieceDef): self._gen_piece(stmt)
|
|
802
|
+
elif isinstance(stmt, BlockDef): self._gen_block(stmt)
|
|
803
|
+
elif isinstance(stmt, TestBench): self._gen_testbench(stmt)
|
|
804
|
+
elif isinstance(stmt, TestGroup): self._gen_testgroup(stmt)
|
|
805
|
+
elif isinstance(stmt, AffStmt): self._gen_aff(stmt)
|
|
806
|
+
|
|
807
|
+
def _gen_known(self, s: KnownStmt):
|
|
808
|
+
# known foo <= bar, baz; → localparam foo = bar;
|
|
809
|
+
self._line(f"localparam {s.name} = {s.values[0]};")
|
|
810
|
+
if len(s.values) > 1:
|
|
811
|
+
self._line(f"// Additional known mappings: {', '.join(s.values[1:])}")
|
|
812
|
+
|
|
813
|
+
def _gen_piece(self, s: PieceDef):
|
|
814
|
+
self._line(f"interface {s.name};")
|
|
815
|
+
self._push()
|
|
816
|
+
for p in s.ports:
|
|
817
|
+
comment = "input" if p.direction == "in" else "output"
|
|
818
|
+
self._line(f"logic {p.name}; // {comment}")
|
|
819
|
+
# generate modports for stricter connectivity
|
|
820
|
+
ins = [p.name for p in s.ports if p.direction == "in"]
|
|
821
|
+
outs = [p.name for p in s.ports if p.direction == "out"]
|
|
822
|
+
if ins or outs:
|
|
823
|
+
self._line(f"modport producer ({'input ' + ', '.join(ins) if ins else ''}"
|
|
824
|
+
f"{', ' if ins and outs else ''}"
|
|
825
|
+
f"{'output ' + ', '.join(outs) if outs else ''});")
|
|
826
|
+
self._pop()
|
|
827
|
+
self._line("endinterface")
|
|
828
|
+
self._line("")
|
|
829
|
+
|
|
830
|
+
def _gen_block(self, s: BlockDef):
|
|
831
|
+
ports_sv: List[str] = []
|
|
832
|
+
for p in s.ports:
|
|
833
|
+
if isinstance(p, Port):
|
|
834
|
+
svdir = "input" if p.direction == "in" else "output"
|
|
835
|
+
ports_sv.append(f"{svdir} logic {p.name}")
|
|
836
|
+
elif isinstance(p, PieceBind):
|
|
837
|
+
ports_sv.append(f"// interface bind: {p.instance} <= {p.interface}")
|
|
838
|
+
self._line(f"module {s.name}(")
|
|
839
|
+
self._push()
|
|
840
|
+
if ports_sv:
|
|
841
|
+
self._line((",\n" + " " * (self.level * self.indent)).join(ports_sv))
|
|
842
|
+
self._pop()
|
|
843
|
+
self._line(");")
|
|
844
|
+
self._push()
|
|
845
|
+
if s.instance:
|
|
846
|
+
self._line(f"// instance name: {s.instance}")
|
|
847
|
+
for st in s.stmts:
|
|
848
|
+
self._gen_block_stmt(st)
|
|
849
|
+
self._pop()
|
|
850
|
+
self._line("endmodule")
|
|
851
|
+
self._line("")
|
|
852
|
+
|
|
853
|
+
def _gen_block_stmt(self, s: Union[VarDecl, AssignStmt, RetStmt, PassParams]):
|
|
854
|
+
if isinstance(s, VarDecl):
|
|
855
|
+
self._line(f"logic {s.name};")
|
|
856
|
+
elif isinstance(s, AssignStmt):
|
|
857
|
+
self._line(f"assign {s.target} = {self._expr(s.expr)};")
|
|
858
|
+
elif isinstance(s, RetStmt):
|
|
859
|
+
if s.range:
|
|
860
|
+
a, b = s.range
|
|
861
|
+
self._line(f"logic [{a}:{b}] {s.name};")
|
|
862
|
+
else:
|
|
863
|
+
self._line(f"logic {s.name};")
|
|
864
|
+
expr = self._expr(s.expr)
|
|
865
|
+
if s.tempassign:
|
|
866
|
+
# tempassign → combinational driver (blocking feel)
|
|
867
|
+
self._line(f"always_comb {s.name} = {expr};")
|
|
868
|
+
else:
|
|
869
|
+
self._line(f"assign {s.name} = {expr};")
|
|
870
|
+
elif isinstance(s, PassParams):
|
|
871
|
+
pstr = ", ".join(s.params)
|
|
872
|
+
self._line(f"{s.module} #({pstr}) {s.instance}();")
|
|
873
|
+
|
|
874
|
+
def _expr(self, e: Expr) -> str:
|
|
875
|
+
if not e.binops:
|
|
876
|
+
return self._operand(e.ops[0])
|
|
877
|
+
parts = [self._operand(e.ops[0])]
|
|
878
|
+
for i, b in enumerate(e.binops):
|
|
879
|
+
parts.append(b)
|
|
880
|
+
parts.append(self._operand(e.ops[i + 1]))
|
|
881
|
+
return " ".join(parts)
|
|
882
|
+
|
|
883
|
+
def _operand(self, o: ExprOp) -> str:
|
|
884
|
+
if o.kind == "string": return o.value
|
|
885
|
+
if o.kind == "int": return str(o.value)
|
|
886
|
+
return str(o.value)
|
|
887
|
+
|
|
888
|
+
def _gen_testbench(self, s: TestBench):
|
|
889
|
+
self._line(f"module {s.name}_tb;")
|
|
890
|
+
self._push()
|
|
891
|
+
self._line(f"{s.target} uut();")
|
|
892
|
+
self._line("")
|
|
893
|
+
# clock / reset helpers (convenience)
|
|
894
|
+
self._line("reg clk = 0;")
|
|
895
|
+
self._line("always #5 clk = ~clk;")
|
|
896
|
+
self._line("reg rst_n;")
|
|
897
|
+
self._line("")
|
|
898
|
+
if self.dump_vcd:
|
|
899
|
+
self._line(f"initial begin")
|
|
900
|
+
self._push()
|
|
901
|
+
self._line(f'$dumpfile("{s.name}.vcd");')
|
|
902
|
+
self._line('$dumpvars(0, uut);')
|
|
903
|
+
self._pop()
|
|
904
|
+
self._line("end")
|
|
905
|
+
self._line("")
|
|
906
|
+
self._line("initial begin")
|
|
907
|
+
self._push()
|
|
908
|
+
self._line("rst_n = 0; #20 rst_n = 1;")
|
|
909
|
+
for st in s.stmts:
|
|
910
|
+
if isinstance(st, (VarDecl, AssignStmt, RetStmt, PassParams)):
|
|
911
|
+
self._gen_block_stmt(st)
|
|
912
|
+
else:
|
|
913
|
+
self._gen_verif(st)
|
|
914
|
+
self._line("$finish;")
|
|
915
|
+
self._pop()
|
|
916
|
+
self._line("end")
|
|
917
|
+
self._pop()
|
|
918
|
+
self._line("endmodule")
|
|
919
|
+
self._line("")
|
|
920
|
+
|
|
921
|
+
def _gen_verif(self, v: VerifCmd):
|
|
922
|
+
if isinstance(v, GetVarsCmd):
|
|
923
|
+
items = [("!" if i.invert else "") + i.name for i in v.items]
|
|
924
|
+
self._line(f'$display("GETVARS: %0t | {", ".join(items)}", $time);')
|
|
925
|
+
elif isinstance(v, WhenCmd):
|
|
926
|
+
self._line(f"if ({v.condition}) begin")
|
|
927
|
+
self._push()
|
|
928
|
+
for c in v.commands:
|
|
929
|
+
self._gen_verif(c)
|
|
930
|
+
self._pop()
|
|
931
|
+
self._line("end")
|
|
932
|
+
elif isinstance(v, OutCmd):
|
|
933
|
+
val = self._operand(v.value)
|
|
934
|
+
self._line(f"#{v.delay} $display({val});")
|
|
935
|
+
elif isinstance(v, PutCmd):
|
|
936
|
+
self._line(f"{v.target} <= {self._expr(v.expr)};")
|
|
937
|
+
elif isinstance(v, ExpectCmd):
|
|
938
|
+
self._line(f"#{v.delay} if (!({self._expr(v.expr)})) $error(\"EXPECT failed at %0t\", $time);")
|
|
939
|
+
elif isinstance(v, PulseCmd):
|
|
940
|
+
self._line(f"// pulse generation: len={v.len_sig}, gap={v.gap_sig}")
|
|
941
|
+
self._line(f"begin")
|
|
942
|
+
self._push()
|
|
943
|
+
self._line(f"repeat ({v.len_sig}) @(posedge clk);")
|
|
944
|
+
self._line(f"// gap")
|
|
945
|
+
self._line(f"repeat ({v.gap_sig}) @(posedge clk);")
|
|
946
|
+
self._pop()
|
|
947
|
+
self._line("end")
|
|
948
|
+
elif isinstance(v, WatchCmd):
|
|
949
|
+
val = self._operand(v.out.value)
|
|
950
|
+
self._line(f"#{v.delay} wait ({v.target} == ({v.source}));")
|
|
951
|
+
self._line(f" $display({val});")
|
|
952
|
+
elif isinstance(v, WriteFileCmd):
|
|
953
|
+
self._line(f"$fopen(\"{v.file_var}.{v.file_ext}\", \"{v.mode}\"); // writefile")
|
|
954
|
+
|
|
955
|
+
def _gen_testgroup(self, s: TestGroup):
|
|
956
|
+
self._line(f"module {s.name};")
|
|
957
|
+
self._push()
|
|
958
|
+
self._line("initial begin")
|
|
959
|
+
self._push()
|
|
960
|
+
self._line(f'$display("=== TESTGROUP: {s.name} ===");')
|
|
961
|
+
for item in s.items:
|
|
962
|
+
if isinstance(item, DoCmd):
|
|
963
|
+
self._line(f"{item.name}_tb.run();")
|
|
964
|
+
elif isinstance(item, SameBlock):
|
|
965
|
+
self._line(f"// parallel group: {', '.join(item.names)}")
|
|
966
|
+
for n in item.names:
|
|
967
|
+
self._line(f"fork {n}_tb.run(); join_none")
|
|
968
|
+
self._line("wait fork;")
|
|
969
|
+
self._pop()
|
|
970
|
+
self._line("end")
|
|
971
|
+
self._pop()
|
|
972
|
+
self._line("endmodule")
|
|
973
|
+
self._line("")
|
|
974
|
+
|
|
975
|
+
def _gen_aff(self, s: AffStmt):
|
|
976
|
+
sens = " or ".join(f"{t.edge} {t.signal}" for t in s.edge_expr.terms)
|
|
977
|
+
self._line(f"always_ff @({sens}) begin")
|
|
978
|
+
self._push()
|
|
979
|
+
for st in s.stmts:
|
|
980
|
+
self._gen_block_stmt(st)
|
|
981
|
+
self._pop()
|
|
982
|
+
self._line("end")
|
|
983
|
+
self._line("")
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
# =============================================================================
|
|
987
|
+
# 6. AST SERIALISER (for --ast mode)
|
|
988
|
+
# =============================================================================
|
|
989
|
+
|
|
990
|
+
def ast_to_dict(obj: Any) -> Any:
|
|
991
|
+
if isinstance(obj, list):
|
|
992
|
+
return [ast_to_dict(x) for x in obj]
|
|
993
|
+
if isinstance(obj, tuple):
|
|
994
|
+
return list(obj)
|
|
995
|
+
if hasattr(obj, "__dataclass_fields__"):
|
|
996
|
+
return {k: ast_to_dict(getattr(obj, k)) for k in obj.__dataclass_fields__}
|
|
997
|
+
return obj
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
# =============================================================================
|
|
1001
|
+
# 7. CLI
|
|
1002
|
+
# =============================================================================
|
|
1003
|
+
|
|
1004
|
+
def main():
|
|
1005
|
+
ap = argparse.ArgumentParser(description="NeoH → SystemVerilog Transpiler")
|
|
1006
|
+
ap.add_argument("input", help="Source .neoh file")
|
|
1007
|
+
ap.add_argument("-o", "--output", help="Output .sv file (default: input.sv)")
|
|
1008
|
+
ap.add_argument("--ast", action="store_true", help="Dump AST as JSON to stdout")
|
|
1009
|
+
ap.add_argument("--check", action="store_true", help="Run semantic analysis only")
|
|
1010
|
+
ap.add_argument("--indent", type=int, default=2, help="Indentation width")
|
|
1011
|
+
ap.add_argument("--dump-vcd", action="store_true", help="Inject VCD dump code into testbenches")
|
|
1012
|
+
args = ap.parse_args()
|
|
1013
|
+
|
|
1014
|
+
src_path = Path(args.input)
|
|
1015
|
+
if not src_path.exists():
|
|
1016
|
+
print(f"Error: file not found: {src_path}", file=sys.stderr)
|
|
1017
|
+
sys.exit(1)
|
|
1018
|
+
|
|
1019
|
+
text = src_path.read_text(encoding="utf-8")
|
|
1020
|
+
|
|
1021
|
+
# ---- parse ----
|
|
1022
|
+
try:
|
|
1023
|
+
tokens = Tokenizer(text).tokenize()
|
|
1024
|
+
ast = Parser(tokens).parse()
|
|
1025
|
+
except SyntaxError as e:
|
|
1026
|
+
print(f"Syntax Error: {e}", file=sys.stderr)
|
|
1027
|
+
sys.exit(1)
|
|
1028
|
+
|
|
1029
|
+
# ---- ast dump ----
|
|
1030
|
+
if args.ast:
|
|
1031
|
+
print(json.dumps(ast_to_dict(ast), indent=2))
|
|
1032
|
+
sys.exit(0)
|
|
1033
|
+
|
|
1034
|
+
# ---- semantic check ----
|
|
1035
|
+
checker = SemanticAnalyzer()
|
|
1036
|
+
ok = checker.analyze(ast)
|
|
1037
|
+
for w in checker.warnings:
|
|
1038
|
+
print(f"Warning: {w}", file=sys.stderr)
|
|
1039
|
+
for e in checker.errors:
|
|
1040
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
1041
|
+
if not ok or args.check:
|
|
1042
|
+
sys.exit(0 if args.check and ok else 1)
|
|
1043
|
+
|
|
1044
|
+
# ---- generate ----
|
|
1045
|
+
gen = SVGenerator(indent=args.indent, dump_vcd=args.dump_vcd)
|
|
1046
|
+
sv = gen.generate(ast)
|
|
1047
|
+
|
|
1048
|
+
out_path = Path(args.output) if args.output else src_path.with_suffix(".sv")
|
|
1049
|
+
out_path.write_text(sv, encoding="utf-8")
|
|
1050
|
+
print(f"Success: {out_path}")
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
if __name__ == "__main__":
|
|
1054
|
+
main()
|
|
1055
|
+
|