Skip to content

Commit 175ee43

Browse files
Merge branch 'develop-error-message-empty-array-struct'. Close #695.
**Description** Copilot allows defining empty arrays (i.e., values of type `Array 0`) and empty structs (i.e., structs with no fields), but using empty arrays or structs in C99 is undefined behavior. Moreover, `copilot-c99` will often crash if you attempt to compile a Copilot specification that uses an empty array or struct. **Type** * Bug: Copilot crashes or generates incorrect code. **Additional context** None. **Requester** * Ryan Scott (Galois) **Method to check presence of bug** The following Copilot specification contains an empty array: ```hs -- EmptyArray.hs {-# LANGUAGE DataKinds #-} {-# LANGUAGE NoImplicitPrelude #-} module Main (main) where import Language.Copilot import Copilot.Compile.C99 (compile) arrs :: Stream (Array 0 Bool) arrs = constant (array []) spec :: Spec spec = trigger "trig" true [arg arrs] main :: IO () main = do spec' <- reify spec compile "empty_array" spec' ``` And the following Copilot specification contains an empty struct: ```hs -- EmptyStruct.hs {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE NoImplicitPrelude #-} module Main (main) where import GHC.Generics (Generic) import Language.Copilot import Copilot.Compile.C99 (compile) data Empty = Empty deriving Generic instance Struct Empty where typeName = typeNameDefault toValues = toValuesDefault updateField = updateFieldDefault instance Typed Empty where typeOf = typeOfDefault structs :: Stream Empty structs = constant Empty spec :: Spec spec = trigger "trig" true [arg structs] main :: IO () main = do spec' <- reify spec compile "empty_struct" spec' ``` Compiling either program will cause `copilot-c99` to crash: ``` $ runghc EmptyArray.hs EmptyArray.hs: NonEmpty.fromList: empty list CallStack (from HasCallStack): error, called at libraries/base/Data/List/NonEmpty.hs:200:15 in base:Data.List.NonEmpty fromList, called at src/Copilot/Compile/C99/Expr.hs:381:3 in copilot-c99-4.6-inplace:Copilot.Compile.C99.Expr $ runghc EmptyStruct.hs EmptyStruct.hs: NonEmpty.fromList: empty list CallStack (from HasCallStack): error, called at libraries/base/Data/List/NonEmpty.hs:200:15 in base:Data.List.NonEmpty fromList, called at src/Copilot/Compile/C99/Expr.hs:375:19 in copilot-c99-4.6-inplace:Copilot.Compile.C99.Expr ``` Note that not all Copilot specifications which contain empty arrays or structs will cause Copilot to crash outright. For instance, the following program contains an empty array, but only in the type of an extern: ```hs -- EmptyArrayExtern.hs {-# LANGUAGE DataKinds #-} {-# LANGUAGE NoImplicitPrelude #-} module Main (main) where import Language.Copilot import Copilot.Compile.C99 (compile) arrs :: Stream (Array 0 Bool) arrs = extern "arrs" Nothing spec :: Spec spec = trigger "trig" true [arg arrs] main :: IO () main = do spec' <- reify spec compile "empty_array_extern" spec' ``` `copilot-c99` will compile this to C code, but using a C compiler such as `gcc` or `clang` will warn you that the resulting C code falls outside of C99 if you use the `-Wpedantic` flag: ``` $ clang -c empty_array_extern.c -o empty_array_extern.o -Wpedantic In file included from empty_array_extern.c:8: ./empty_array_extern.h:1:18: warning: zero size arrays are an extension [-Wzero-length-array] 1 | extern bool arrs[(0)]; | ^~~ ./empty_array_extern.h:2:26: warning: zero size arrays are an extension [-Wzero-length-array] 2 | void trig(bool trig_arg0[(0)]); | ^~~ empty_array_extern.c:10:22: warning: zero size arrays are an extension [-Wzero-length-array] 10 | static bool arrs_cpy[(0)]; | ^~~ empty_array_extern.c:16:49: warning: zero size arrays are an extension [-Wzero-length-array] 16 | static void trig_0_arg0(bool trig_0_arg0_output[(0)]) { | ^~~ empty_array_extern.c:21:25: warning: zero size arrays are an extension [-Wzero-length-array] 21 | bool trig_0_arg_temp0[(0)]; | ^~~ 5 warnings generated. ``` **Expected result** A clear error message is raised either by the Copilot compiler or GCC indicating that the code produced involving empty arrays or structs is not valid. The following Dockerfile checks that compiling an empty struct or array raises fails during compilation, after which it prints the message Success: ``` --- Dockerfile-verify-695 FROM ubuntu:focal ENV DEBIAN_FRONTEND=noninteractive SHELL ["/bin/bash", "-c"] RUN apt-get update RUN apt-get install --yes software-properties-common RUN add-apt-repository ppa:hvr/ghc RUN apt-get update RUN apt-get install --yes ghc-8.6.5 cabal-install-2.4 RUN apt-get install --yes libz-dev git alex happy ENV PATH=/opt/ghc/8.6.5/bin:/opt/cabal/2.4/bin:$PWD/.cabal-sandbox/bin:$PATH RUN cabal update RUN cabal v1-sandbox init ADD EmptyArray.hs /tmp/ ADD EmptyStruct.hs /tmp/ CMD git clone $REPO && cd $NAME && git checkout $COMMIT && cd .. \ && cabal v1-install \ $NAME/copilot \ $NAME/copilot-c99 \ $NAME/copilot-core \ $NAME/copilot-interpreter \ $NAME/copilot-language \ $NAME/copilot-libraries \ $NAME/copilot-prettyprinter \ $NAME/copilot-theorem \ && ! cabal v1-exec -- runhaskell /tmp/EmptyArray.hs \ && ! cabal v1-exec -- runhaskell /tmp/EmptyStruct.hs \ && echo "Success" --- EmptyArray.hs {-# LANGUAGE DataKinds #-} {-# LANGUAGE NoImplicitPrelude #-} module Main (main) where import Copilot.Compile.C99 (compile) import Language.Copilot main :: IO () main = do spec' <- reify spec compile "empty_array" spec' spec :: Spec spec = trigger "trig" true [arg arrs] arrs :: Stream (Array 0 Bool) arrs = constant (array []) --- EmptyStruct.hs {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE NoImplicitPrelude #-} module Main (main) where import GHC.Generics (Generic) import Copilot.Compile.C99 (compile) import Language.Copilot main :: IO () main = do spec' <- reify spec compile "empty_struct" spec' spec :: Spec spec = trigger "trig" true [arg structs] structs :: Stream Empty structs = constant Empty -- * Empty struct type data Empty = Empty deriving Generic instance Struct Empty where typeName = typeNameDefault toValues = toValuesDefault updateField = updateFieldDefault instance Typed Empty where typeOf = typeOfDefault ``` Command (substitute variables based on new path after merge): ``` $ docker run -e "REPO=https://github.com/Copilot-Language/copilot" -e "NAME=copilot" -e "COMMIT=<HASH>" -it copilot-verify-695 ``` **Solution implemented** Modify `copilot-c99`'s `constStruct` and `constArray` to raise an error if a struct or array passed as argument is empty. Modify `copilot-c99`'s `transType` to raise an error if a struct or array passed as argument is empty. Add test cases to `copilot-c99` to ensure that Copilot specifications with empty arrays or structs are rejected by the C99 backend. **Further notes** None.
2 parents 60e30ec + a012695 commit 175ee43

5 files changed

Lines changed: 107 additions & 11 deletions

File tree

copilot-c99/CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2026-02-16
2+
* Raise error when compiling empty arrays or structs. (#695)
3+
14
2026-01-07
25
* Version bump (4.6.1). (#705)
36

copilot-c99/src/Copilot/Compile/C99/Error.hs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
--
66
-- Custom functions to report error messages to users.
77
module Copilot.Compile.C99.Error
8-
( impossible )
8+
( impossible
9+
, errorEmptyStruct
10+
, errorZeroLengthArray
11+
)
912
where
1013

1114
-- | Report an error due to a bug in Copilot.
@@ -18,3 +21,17 @@ impossible function package =
1821
++ ". Please file an issue at "
1922
++ "https://github.com/Copilot-Language/copilot/issues"
2023
++ " or email the maintainers at <ivan.perezdominguez@nasa.gov>"
24+
25+
-- | Report an error when attempting to compile a zero-length array to C99.
26+
-- C99 does not support zero-length arrays, so Copilot cannot compile
27+
-- specifications that use them.
28+
errorZeroLengthArray :: a
29+
errorZeroLengthArray =
30+
error "copilot-c99: Cannot compile zero-length arrays to C99.\n"
31+
32+
-- | Report an error when attempting to compile an empty struct to C99.
33+
-- C99 does not support empty structs, so Copilot cannot compile
34+
-- specifications that use them.
35+
errorEmptyStruct :: a
36+
errorEmptyStruct =
37+
error "copilot-c99: Cannot compile empty structs to C99.\n"

copilot-c99/src/Copilot/Compile/C99/Expr.hs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Copilot.Core ( Array, Expr (..), Field (..), Op1 (..), Op2 (..),
1818
arrayElems, toValues, typeLength, typeSize )
1919

2020
-- Internal imports
21-
import Copilot.Compile.C99.Error ( impossible )
21+
import Copilot.Compile.C99.Error ( impossible, errorEmptyStruct, errorZeroLengthArray )
2222
import Copilot.Compile.C99.Name ( exCpyName, streamAccessorName )
2323
import Copilot.Compile.C99.Type ( transLocalVarDeclType, transType,
2424
transTypeName )
@@ -371,14 +371,23 @@ constFieldInit (Value ty (Field val)) = C.InitItem Nothing $ constInit ty val
371371

372372
-- | Transform a Copilot Struct, based on the struct fields, into a list of C99
373373
-- initializer values.
374+
--
375+
-- Raises an error if the struct is empty, as C99 does not support structs with
376+
-- no fields.
374377
constStruct :: [Value a] -> NonEmpty.NonEmpty C.InitItem
375-
constStruct val = NonEmpty.fromList $ map constFieldInit val
378+
constStruct val = case val of
379+
[] -> errorEmptyStruct
380+
_ -> NonEmpty.fromList $ map constFieldInit val
376381

377382
-- | Transform a Copilot Array, based on the element values and their type,
378383
-- into a list of C99 initializer values.
384+
--
385+
-- Raises an error if the array is empty, as C99 does not support zero-length
386+
-- arrays.
379387
constArray :: Type a -> [a] -> NonEmpty.NonEmpty C.InitItem
380-
constArray ty =
381-
NonEmpty.fromList . map (C.InitItem Nothing . constInit ty)
388+
constArray ty xs = case xs of
389+
[] -> errorZeroLengthArray
390+
_ -> NonEmpty.fromList $ map (C.InitItem Nothing . constInit ty) xs
382391

383392
-- | Explicitly cast a C99 value to a type.
384393
explicitTy :: Type a -> C.Expr -> C.Expr

copilot-c99/src/Copilot/Compile/C99/Type.hs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ module Copilot.Compile.C99.Type
1212
import qualified Language.C99.Simple as C
1313

1414
-- Internal imports: Copilot
15-
import Copilot.Core ( Type (..), typeLength, typeName )
15+
import Copilot.Core ( Struct (..), Type (..), typeLength,
16+
typeName )
17+
import Copilot.Compile.C99.Error ( errorEmptyStruct, errorZeroLengthArray )
1618

1719
-- | Translate a Copilot type to a C99 type.
1820
transType :: Type a -> C.Type
@@ -28,10 +30,12 @@ transType ty = case ty of
2830
Word64 -> C.TypeSpec $ C.TypedefName "uint64_t"
2931
Float -> C.TypeSpec C.Float
3032
Double -> C.TypeSpec C.Double
31-
Array ty' -> C.Array (transType ty') len
33+
Array ty' | typeLength ty == 0 -> errorZeroLengthArray
34+
| otherwise -> C.Array (transType ty') len
3235
where
3336
len = Just $ C.LitInt $ fromIntegral $ typeLength ty
34-
Struct s -> C.TypeSpec $ C.Struct (typeName s)
37+
Struct s | null (toValues s) -> errorEmptyStruct
38+
| otherwise -> C.TypeSpec $ C.Struct (typeName s)
3539

3640
-- | Translate a Copilot type to a valid (local) variable declaration C99 type.
3741
--

copilot-c99/tests/Test/Copilot/Compile/C99.hs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ module Test.Copilot.Compile.C99
88

99
-- External imports
1010
import Control.Arrow ((&&&))
11-
import Control.Exception (IOException, catch)
12-
import Control.Monad (when)
11+
import Control.Exception (ErrorCall (..), IOException, catch,
12+
try)
13+
import Control.Monad (unless, when)
1314
import Data.Bits (Bits, complement)
14-
import Data.List (intercalate)
15+
import Data.List (intercalate, isSubsequenceOf)
1516
import Data.Type.Equality (testEquality)
1617
import Data.Typeable (Proxy (..), (:~:) (Refl))
1718
import GHC.TypeLits (KnownNat, natVal)
@@ -24,7 +25,9 @@ import System.Posix.Temp (mkdtemp)
2425
import System.Process (callProcess, readProcess)
2526
import System.Random (Random)
2627
import Test.Framework (Test, testGroup)
28+
import Test.Framework.Providers.HUnit (testCase)
2729
import Test.Framework.Providers.QuickCheck2 (testProperty)
30+
import Test.HUnit (Assertion, assertFailure)
2831
import Test.QuickCheck (Arbitrary, Gen, Property,
2932
arbitrary, choose, elements,
3033
forAll, forAllBlind, frequency,
@@ -50,6 +53,7 @@ tests =
5053
, testProperty "Compile specification in custom dir" testCompileCustomDir
5154
, testProperty "Run specification" testRun
5255
, testProperty "Run and compare results" testRunCompare
56+
, testCase "Reject empty arrays and structs" testRejectEmpties
5357
]
5458

5559
-- * Individual tests
@@ -137,6 +141,65 @@ testCompileCustomDir = ioProperty $ do
137141

138142
args = []
139143

144+
data Empty = Empty
145+
146+
instance Struct Empty where
147+
typeName _ = ""
148+
toValues _ = []
149+
150+
instance Typed Empty where
151+
typeOf = Struct Empty
152+
153+
-- | Test that compiling a spec with an empty array or struct raises an error.
154+
-- C99 does not support zero-length arrays or structs without fields, so Copilot
155+
-- should report an error during compilation.
156+
testRejectEmpties :: Assertion
157+
testRejectEmpties = do
158+
tmpDir <- getTemporaryDirectory
159+
setCurrentDirectory tmpDir
160+
161+
r1 <- try (compile "copilot_test" $ spec t1) :: IO (Either ErrorCall ())
162+
unless (isErrZeroArray r1) $ assertFailure "Const zero-length array"
163+
164+
r2 <- try (compile "copilot_test" $ spec t2) :: IO (Either ErrorCall ())
165+
unless (isErrZeroArray r2) $ assertFailure "Extern zero-length array"
166+
167+
r3 <- try (compile "copilot_test" $ spec t3) :: IO (Either ErrorCall ())
168+
unless (isErrEmptyStruct r3) $ assertFailure "Const empty struct"
169+
170+
r4 <- try (compile "copilot_test" $ spec t4) :: IO (Either ErrorCall ())
171+
unless (isErrEmptyStruct r4) $ assertFailure "Extern empty struct"
172+
173+
where
174+
175+
spec a = Spec streams observers (triggers a) properties
176+
177+
streams = []
178+
observers = []
179+
triggers a = [ Trigger "trig" (Const Bool True) a ]
180+
properties = []
181+
182+
t1 = [ UExpr arrTy (Const arrTy (array [])) ]
183+
t2 = [ UExpr arrTy (ExternVar arrTy "arrs" Nothing) ]
184+
t3 = [ UExpr structTy (Const structTy Empty) ]
185+
t4 = [ UExpr structTy (ExternVar structTy "structs" Nothing) ]
186+
187+
arrTy :: Type (Array 0 Bool)
188+
arrTy = Array Bool
189+
190+
structTy :: Type Empty
191+
structTy = Struct Empty
192+
193+
isErrZeroArray :: Either ErrorCall a -> Bool
194+
isErrZeroArray e = case e of
195+
Left (ErrorCall msg) -> "zero-length array" `isSubsequenceOf` msg
196+
_ -> False
197+
198+
isErrEmptyStruct :: Either ErrorCall a -> Bool
199+
isErrEmptyStruct e = case e of
200+
Left (ErrorCall msg) -> "empty struct" `isSubsequenceOf` msg
201+
_ -> False
202+
140203
-- | Test compiling a spec and running the resulting program.
141204
--
142205
-- The actual behavior is ignored.

0 commit comments

Comments
 (0)