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 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
+