diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b52be2..b3df42a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ intentional width-extending operations such as `+ 0` and `* 1` * Fixed forced conversion to `reg` of data sensed in an edge-controlled procedural assignment +* `always_comb` and `always_latch` now generate explicit sensitivity lists where + necessary because of calls to functions which reference non-local data ## v0.0.9 diff --git a/src/Convert/AlwaysKW.hs b/src/Convert/AlwaysKW.hs index 70ffefc..7e782a2 100644 --- a/src/Convert/AlwaysKW.hs +++ b/src/Convert/AlwaysKW.hs @@ -3,24 +3,224 @@ - - Conversion for `always_latch`, `always_comb`, and `always_ff` - - - `always_latch` -> `always @*` - - `always_comb` -> `always @*` - - `always_ff` -> `always` + - `always_comb` and `always_latch` become `always @*`, or produce an explicit + - sensitivity list if they need to pick up sensitivities from the functions + - they call. `always_ff` simply becomes `always`. + - + - TODO: `always_comb` blocks do not run at time zero -} module Convert.AlwaysKW (convert) where +import Control.Monad.State.Strict +import Control.Monad.Writer.Strict +import Data.Maybe (fromMaybe, mapMaybe) + +import Convert.Scoper import Convert.Traverse import Language.SystemVerilog.AST convert :: [AST] -> [AST] -convert = map $ traverseDescriptions $ traverseModuleItems replaceAlwaysKW +convert = map $ traverseDescriptions traverseDescription -replaceAlwaysKW :: ModuleItem -> ModuleItem -replaceAlwaysKW (AlwaysC AlwaysLatch stmt) = - AlwaysC Always $ Timing (Event EventStar) stmt -replaceAlwaysKW (AlwaysC AlwaysComb stmt) = - AlwaysC Always $ Timing (Event EventStar) stmt -replaceAlwaysKW (AlwaysC AlwaysFF stmt) = - AlwaysC Always stmt -replaceAlwaysKW other = other +traverseDescription :: Description -> Description +traverseDescription description@Part{} = + evalState (evalScoperT $ scopePart op description) mempty + where op = traverseModuleItem >=> scoper +traverseDescription description = description + +type SC = ScoperT Kind (State (Any, [Expr])) + +type PortDir = (Identifier, Direction) + +data Kind + = Const Expr + | Var + | Proc [Expr] [PortDir] + deriving Eq + +scoper :: ModuleItem -> SC ModuleItem +scoper = scopeModuleItem traverseDecl return traverseGenItem traverseStmt + +-- track declarations and visit expressions they contain +traverseDecl :: Decl -> SC Decl +traverseDecl decl = do + case decl of + Param s _ x e -> do + -- handle references to local constants + e' <- if s == Localparam + then scopeExpr e + else return Nil + insertElem x $ Const e' + ParamType _ x _ -> insertElem x $ Const Nil + Variable _ _ x _ _ -> do + -- don't let the second visit of a function or task overwrite the + -- Proc entry that was just generated + details <- lookupLocalIdentM x + case details of + Just (_, _, Proc{}) -> return () + _ -> insertElem x Var + Net _ _ _ _ x _ _ -> insertElem x Var + CommentDecl{} -> return () + traverseDeclExprsM traverseExpr decl + +-- track expressions and subroutines in a statement +traverseStmt :: Stmt -> SC Stmt +traverseStmt (Subroutine expr args) = + traverseCall Subroutine expr args +traverseStmt stmt = traverseStmtExprsM traverseExpr stmt + +-- visit tasks, functions, and always blocks in generate scopes +traverseGenItem :: GenItem -> SC GenItem +traverseGenItem (GenModuleItem item) = + traverseModuleItem item >>= return . GenModuleItem +traverseGenItem other = return other + +-- identify variables referenced within an expression +traverseExpr :: Expr -> SC Expr +traverseExpr (Call expr args) = + traverseCall Call expr args +traverseExpr expr = do + prefix <- embedScopes longestStaticPrefix expr + case prefix of + Just expr' -> push (Any False, [expr']) >> return expr + _ -> traverseSinglyNestedExprsM traverseExpr expr + +-- turn a reference to a variable into a canonicalized longest static prefix, if +-- possible, per IEEE 1800-2017 Section 11.5.3 +longestStaticPrefix :: Scopes Kind -> Expr -> Maybe Expr +longestStaticPrefix scopes expr@Ident{} = + asVar scopes expr +longestStaticPrefix scopes (Range expr mode (l, r)) = do + expr' <- longestStaticPrefix scopes expr + l' <- asConst scopes l + r' <- asConst scopes r + Just $ Range expr' mode (l', r') +longestStaticPrefix scopes (Bit expr idx) = do + expr' <- longestStaticPrefix scopes expr + idx' <- asConst scopes idx + Just $ Bit expr' idx' +longestStaticPrefix scopes orig@(Dot expr field) = + case asVar scopes orig of + Just orig' -> Just orig' + _ -> do + expr' <- longestStaticPrefix scopes expr + Just $ Dot expr' field +longestStaticPrefix _ _ = + Nothing + +-- lookup an expression as an outwardly-visible variable +asVar :: Scopes Kind -> Expr -> Maybe Expr +asVar scopes expr = do + (accesses, _, Var) <- lookupElem scopes expr + if visible accesses + then Just $ accessesToExpr accesses + else Nothing + +visible :: [Access] -> Bool +visible = not . elem (Access "" Nil) + +-- lookup an expression as a hoist-able constant +asConst :: Scopes Kind -> Expr -> Maybe Expr +asConst scopes expr = + case runWriter $ asConstRaw scopes expr of + (expr', Any False) -> Just expr' + _ -> Nothing + +asConstRaw :: Scopes Kind -> Expr -> Writer Any Expr +asConstRaw scopes expr = + case lookupElem scopes expr of + Just (_, _, Const Nil) -> recurse + Just (_, _, Const expr') -> asConstRaw scopes expr' + Just{} -> tell (Any True) >> return Nil + Nothing -> recurse + where + recurse = traverseSinglyNestedExprsM (asConstRaw scopes) expr + +-- special handling for subroutine invocations and function calls +traverseCall :: (Expr -> Args -> a) -> Expr -> Args -> SC a +traverseCall constructor expr args = do + details <- lookupElemM expr + expr' <- traverseExpr expr + args' <- case details of + Just (_, _, Proc exprs ps) -> do + when (not $ null exprs) $ + push (Any True, exprs) + traverseArgs ps args + _ -> traverseArgs [] args + return $ constructor expr' args' + +-- treats output ports as assignment-like contexts +traverseArgs :: [PortDir] -> Args -> SC Args +traverseArgs ps (Args pnArgs kwArgs) = do + pnArgs' <- zipWithM usingPN [0..] pnArgs + kwArgs' <- mapM usingKW kwArgs + return (Args pnArgs' kwArgs') + where + + usingPN :: Int -> Expr -> SC Expr + usingPN key val = do + if dir == Output + then return val + else traverseExpr val + where dir = if key < length ps + then snd $ ps !! key + else Input + + usingKW :: (Identifier, Expr) -> SC (Identifier, Expr) + usingKW (key, val) = do + val' <- if dir == Output + then return val + else traverseExpr val + return (key, val') + where dir = fromMaybe Input $ lookup key ps + +-- append to the non-local expression state +push :: (Any, [Expr]) -> SC () +push x = lift $ modify' (x <>) + +-- custom traversal which converts SystemVerilog `always` keywords and tracks +-- information about task and functions +traverseModuleItem :: ModuleItem -> SC ModuleItem +traverseModuleItem (AlwaysC AlwaysLatch stmt) = do + e <- fmap toEvent $ findNonLocals $ Initial stmt + return $ AlwaysC Always $ Timing (Event e) stmt +traverseModuleItem (AlwaysC AlwaysComb stmt) = do + e <- fmap toEvent $ findNonLocals $ Initial stmt + return $ AlwaysC Always $ Timing (Event e) stmt +traverseModuleItem (AlwaysC AlwaysFF stmt) = + return $ AlwaysC Always stmt +traverseModuleItem item@(MIPackageItem (Function _ _ x decls _)) = do + (_, s) <- findNonLocals item + insertElem x $ Proc s (ports decls) + return item +traverseModuleItem item@(MIPackageItem (Task _ x decls _)) = do + insertElem x $ Proc [] (ports decls) + return item +traverseModuleItem other = return other + +toEvent :: (Bool, [Expr]) -> Event +toEvent (False, _) = EventStar +toEvent (True, exprs) = + EventExpr $ foldl1 EventExprOr $ map (EventExprEdge NoEdge) exprs + +-- turn a list of port declarations into a port direction map +ports :: [Decl] -> [PortDir] +ports = filter ((/= Local) . snd) . map port + +port :: Decl -> PortDir +port (Variable d _ x _ _) = (x, d) +port (Net d _ _ _ x _ _) = (x, d) +port _ = ("", Local) + +-- get a list of non-local variables referenced within a module item, and +-- whether or not this module item references any functions which themselves +-- reference non-local variables +findNonLocals :: ModuleItem -> SC (Bool, [Expr]) +findNonLocals item = do + scopes <- get + lift $ put mempty + _ <- scoper item + (anys, exprs) <- lift get + let nonLocals = mapMaybe (longestStaticPrefix scopes) exprs + return (getAny anys, nonLocals) diff --git a/test/core/always_prefix.sv b/test/core/always_prefix.sv new file mode 100644 index 0000000..de1540f --- /dev/null +++ b/test/core/always_prefix.sv @@ -0,0 +1,2 @@ +`define ALWAYS(trigger) always_comb +`include "always_prefix.vh" diff --git a/test/core/always_prefix.v b/test/core/always_prefix.v new file mode 100644 index 0000000..f2a2c1f --- /dev/null +++ b/test/core/always_prefix.v @@ -0,0 +1,2 @@ +`define ALWAYS(trigger) always @(trigger) +`include "always_prefix.vh" diff --git a/test/core/always_prefix.vh b/test/core/always_prefix.vh new file mode 100644 index 0000000..28bb2b5 --- /dev/null +++ b/test/core/always_prefix.vh @@ -0,0 +1,43 @@ +module mod( + input wire [3:0] idx, + input wire [14:0] data +); + localparam Y = 2; + localparam X = 10000; + +`define TEST(expr, trigger, extra) \ + if (1) begin \ + function automatic f; \ + input reg ignored; \ + localparam X = Y + 1; \ + localparam THREE = X; \ + f = expr; \ + endfunction \ + `ALWAYS(trigger) begin : blk \ + localparam ZERO = 0; \ + $display(`"%2d %b expr trigger`", \ + $time, f(ZERO) extra); \ + end \ + end + +`define TEST_SIMPLE(expr) `TEST(expr, expr, ) + + `TEST_SIMPLE(data) + `TEST_SIMPLE(data[1]) + `TEST_SIMPLE(data[4]) + `TEST_SIMPLE(data[4:1]) + `TEST_SIMPLE(data[10:1]) + +localparam ONE = 1; +parameter FOUR = 4; + `TEST_SIMPLE(data[ONE]) + `TEST_SIMPLE(data[FOUR]) + `TEST_SIMPLE(data[FOUR:ONE]) + + `TEST(data[idx], data or idx, ) + `TEST(data[idx+:2], data or idx, ) + + `TEST(data[THREE], data[3], ) + `TEST(data[ignored], data, ) + `TEST(data[THREE], data[0] or data[3], & data[0]) +endmodule diff --git a/test/core/always_prefix_tb.v b/test/core/always_prefix_tb.v new file mode 100644 index 0000000..bdb9d54 --- /dev/null +++ b/test/core/always_prefix_tb.v @@ -0,0 +1,18 @@ +module top; + reg [3:0] idx; + reg [14:0] data; + mod m(idx, data); + initial begin + #1 data = 0; + #1 idx = 0; + #1 data[0] = 1; + #1 data[4] = 1; + #1 data[5] = 1; + #1 data[3] = 1; + #1 data[8] = 1; + #1 idx = 0; + #1 idx = 1; + #1 data[0] = 0; + #1 data[0] = 1; + end +endmodule diff --git a/test/core/always_sense.sv b/test/core/always_sense.sv new file mode 100644 index 0000000..9bdbac9 --- /dev/null +++ b/test/core/always_sense.sv @@ -0,0 +1,78 @@ +module mod( + input wire inp1, inp2, + output reg out1, out2, out3, out4, out5, out6, out7, out8, out9, outA, outB +); + localparam ZERO = 0; + + task automatic t; + output reg o; + o = inp1; + endtask + + function automatic flop; + input reg i; + flop = i; + endfunction + function automatic flip; + input reg i; + flip = flop(~i); + endfunction + + function automatic f; + input reg i; // ignored + f = inp2; + endfunction + function automatic g; + input reg inp1; // ignored + g = f(ZERO) & mod.inp1; + endfunction + + function void u; + output reg o; + o = inp1; + endfunction + + task automatic asgn; + output reg o; + input reg i; + o = i; + endtask + + always_comb + t(out1); + always_comb + out2 = f(ZERO); + always_comb + out3 = f(ZERO) & inp1; + always_comb + out4 = g(ZERO); + always_comb + out5 = flip(inp1); + always_comb begin + reg x; + x = g(ZERO); + out6 = x; + end + always_comb + u(out7); + parameter ONE = 1; + if (ONE) + always_comb begin + asgn(out8, flip(inp1)); + out9 = f(ZERO); + end + always_latch + if (inp1) + outA = f(ZERO); + + struct packed { + logic x, y; + } s; + assign s = {inp1, inp2}; + function automatic h; + input reg i; // ignored + h = s.y; + endfunction + always_comb + outB = h(ZERO); +endmodule diff --git a/test/core/always_sense.v b/test/core/always_sense.v new file mode 100644 index 0000000..e88392c --- /dev/null +++ b/test/core/always_sense.v @@ -0,0 +1,76 @@ +module mod( + input wire inp1, inp2, + output reg out1, out2, out3, out4, out5, out6, out7, out8, out9, outA, outB +); + localparam ZERO = 0; + + task automatic t; + output reg o; + o = inp1; + endtask + + function automatic flop; + input reg i; + flop = i; + endfunction + function automatic flip; + input reg i; + flip = flop(~i); + endfunction + + function automatic f; + input reg i; // ignored + f = inp2; + endfunction + function automatic g; + input reg i; // ignored + g = f(ZERO) & inp1; + endfunction + + task automatic u; + output reg o; + o = inp1; + endtask + + task automatic asgn; + output reg o; + input reg i; + o = i; + endtask + + always @* + t(out1); + always @(inp2) + out2 = f(ZERO); + always @(inp1, inp2) + out3 = f(ZERO) & inp1; + always @(inp1, inp2) + out4 = g(ZERO); + always @* + out5 = flip(inp1); + always @(inp1, inp2) begin : blk + reg x; + x = g(ZERO); + out6 = x; + end + always @(inp1) + u(out7); + parameter ONE = 1; + if (ONE) + always @(inp1, inp2) begin + asgn(out8, flip(inp1)); + out9 = f(ZERO); + end + always @(inp1, inp2) + if (inp1) + outA = f(ZERO); + + wire [1:0] s; + assign s = {inp1, inp2}; + function automatic h; + input reg i; // ignored + h = s[0]; + endfunction + always @(s[0]) + outB = h(ZERO); +endmodule diff --git a/test/core/always_sense_tb.v b/test/core/always_sense_tb.v new file mode 100644 index 0000000..6dd6af1 --- /dev/null +++ b/test/core/always_sense_tb.v @@ -0,0 +1,26 @@ +module top; + reg inp1, inp2; + wire out1, out2, out3, out4, out5, out6, out7, out8, out9, outA, outB; + mod m(inp1, inp2, out1, out2, out3, out4, out5, out6, out7, out8, out9, outA, outB); + initial begin + $monitor(inp1, inp2, + out1, out2, out3, out4, out5, out6, out7, out8, out9, outA, outB); + repeat (2) begin + #1 inp1 = 0; + #1 inp2 = 0; + #1 inp2 = 1; + + #1 inp1 = 1; + #1 inp2 = 0; + #1 inp2 = 1; + + #1 inp2 = 0; + #1 inp1 = 0; + #1 inp1 = 1; + + #1 inp2 = 1; + #1 inp1 = 0; + #1 inp1 = 1; + end + end +endmodule