diff options
author | Timothee Cour <timothee.cour2@gmail.com> | 2020-04-23 02:24:09 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-23 11:24:09 +0200 |
commit | 66db9de714be7c2f4cf1f2fb0a0a99145baf6acb (patch) | |
tree | 8675842299fe22ad237b06bb923a880789c9faa4 | |
parent | 5c534b2943145106db99212fb9bb75fde289b6f5 (diff) | |
download | Nim-66db9de714be7c2f4cf1f2fb0a0a99145baf6acb.tar.gz |
CT sizeof(+friends) for {.importc, completeStruct.} types, enable ABI static checks (#13926)
* -d:checkabi obsolete (ABI check now enforced); add `addTypeHeader` helper * cleanups * import sizeof at CT for {.completeType.} * address comments; revert default enabling of -d:checkAbi for now * mimportc_size_check.nim => msizeof5.nim; merge mabi_check.nim into msizeof5.nim; refactor * all pragmas in errmsgs should be written: '.importc' (un-ambiguous and less verbose than {.importc.})
-rw-r--r-- | compiler/ast.nim | 3 | ||||
-rw-r--r-- | compiler/ccgmerge.nim | 4 | ||||
-rw-r--r-- | compiler/ccgtypes.nim | 20 | ||||
-rw-r--r-- | compiler/cgendata.nim | 8 | ||||
-rw-r--r-- | compiler/pragmas.nim | 6 | ||||
-rw-r--r-- | compiler/sizealignoffsetimpl.nim | 3 | ||||
-rw-r--r-- | compiler/types.nim | 16 | ||||
-rw-r--r-- | compiler/vmgen.nim | 9 | ||||
-rw-r--r-- | compiler/wordrecg.nim | 7 | ||||
-rw-r--r-- | doc/nimc.rst | 5 | ||||
-rw-r--r-- | lib/nimbase.h | 4 | ||||
-rw-r--r-- | tests/misc/msizeof5.nim | 131 | ||||
-rw-r--r-- | tests/misc/tsizeof2.nim | 2 | ||||
-rw-r--r-- | tests/trunner.nim | 42 |
14 files changed, 221 insertions, 39 deletions
diff --git a/compiler/ast.nim b/compiler/ast.nim index 110ee79e3..2e3169665 100644 --- a/compiler/ast.nim +++ b/compiler/ast.nim @@ -553,6 +553,9 @@ type # If it has one, t.destructor is not nil. tfAcyclic # object type was annotated as .acyclic tfIncompleteStruct # treat this type as if it had sizeof(pointer) + tfCompleteStruct + # (for importc types); type is fully specified, allowing to compute + # sizeof, alignof, offsetof at CT TTypeFlags* = set[TTypeFlag] diff --git a/compiler/ccgmerge.nim b/compiler/ccgmerge.nim index 2be34aace..c12d6c0b2 100644 --- a/compiler/ccgmerge.nim +++ b/compiler/ccgmerge.nim @@ -48,6 +48,10 @@ const NimMergeEndMark = "/*\tNIM_merge_END:*/" proc genSectionStart*(fs: TCFileSection; conf: ConfigRef): Rope = + # useful for debugging and only adds at most a few lines in each file + result.add("\n/* section: ") + result.add(CFileSectionNames[fs]) + result.add(" */\n") if compilationCachePresent(conf): result = nil result.add("\n/*\t") diff --git a/compiler/ccgtypes.nim b/compiler/ccgtypes.nim index 46949831e..c4836c447 100644 --- a/compiler/ccgtypes.nim +++ b/compiler/ccgtypes.nim @@ -247,8 +247,13 @@ proc cacheGetType(tab: TypeCache; sig: SigHash): Rope = result = tab.getOrDefault(sig) proc addAbiCheck(m: BModule, t: PType, name: Rope) = - if isDefined(m.config, "checkabi") and (let size = getSize(m.config, t); size != szUnknownSize): - m.s[cfsTypeInfo].addf("NIM_CHECK_SIZE($1, $2);$n", [name, rope(size)]) + if isDefined(m.config, "checkAbi") and (let size = getSize(m.config, t); size != szUnknownSize): + var msg = "backend & Nim disagree on size for: " + msg.addTypeHeader(m.config, t) + var msg2 = "" + msg2.addQuoted msg # not a hostspot so extra allocation doesn't matter + m.s[cfsTypeInfo].addf("NIM_STATIC_ASSERT(sizeof($1) == $2, $3);$n", [name, rope(size), msg2.rope]) + # see `testCodegenABICheck` for example error message it generates proc ccgIntroducedPtr(conf: ConfigRef; s: PSym, retType: PType): bool = var pt = skipTypes(s.typ, typedescInst) @@ -328,7 +333,6 @@ proc getSimpleTypeDesc(m: BModule, typ: PType): Rope = let sig = hashType typ if cacheGetType(m.typeCache, sig) == nil: m.typeCache[sig] = result - addAbiCheck(m, typ, result) proc pushType(m: BModule, typ: PType) = for i in 0..high(m.typeStack): @@ -676,6 +680,7 @@ proc resolveStarsInCppType(typ: PType, idx, stars: int): PType = proc getTypeDescAux(m: BModule, origTyp: PType, check: var IntSet): Rope = # returns only the type's name + var t = origTyp.skipTypes(irrelevantForBackend-{tyOwned}) if containsOrIncl(check, t.id): if not (isImportedCppType(origTyp) or isImportedCppType(t)): @@ -686,6 +691,11 @@ proc getTypeDescAux(m: BModule, origTyp: PType, check: var IntSet): Rope = if t.sym != nil: useHeader(m, t.sym) if t != origTyp and origTyp.sym != nil: useHeader(m, origTyp.sym) let sig = hashType(origTyp) + + defer: # defer is the simplest in this case + if isImportedType(t) and not m.typeABICache.containsOrIncl(sig): + addAbiCheck(m, t, result) + result = getTypePre(m, t, sig) if result != nil: excl(check, t.id) @@ -818,7 +828,6 @@ proc getTypeDescAux(m: BModule, origTyp: PType, check: var IntSet): Rope = let foo = getTypeDescAux(m, t[1], check) m.s[cfsTypes].addf("typedef $1 $2[$3];$n", [foo, result, rope(n)]) - else: addAbiCheck(m, t, result) of tyObject, tyTuple: if isImportedCppType(t) and origTyp.kind == tyGenericInst: let cppName = getTypeName(m, t, sig) @@ -879,7 +888,8 @@ proc getTypeDescAux(m: BModule, origTyp: PType, check: var IntSet): Rope = else: getTupleDesc(m, t, result, check) if not isImportedType(t): m.s[cfsTypes].add(recdesc) - elif tfIncompleteStruct notin t.flags: addAbiCheck(m, t, result) + elif tfIncompleteStruct notin t.flags: + discard # addAbiCheck(m, t, result) # already handled elsewhere of tySet: # Don't use the imported name as it may be scoped: 'Foo::SomeKind' result = $t.kind & '_' & t.lastSon.typeName & $t.lastSon.hashType diff --git a/compiler/cgendata.nim b/compiler/cgendata.nim index bf7daeea6..96e382873 100644 --- a/compiler/cgendata.nim +++ b/compiler/cgendata.nim @@ -11,7 +11,7 @@ import ast, ropes, options, intsets, - tables, ndi, lineinfos, pathutils, modulegraphs + tables, ndi, lineinfos, pathutils, modulegraphs, sets type TLabel* = Rope # for the C generator a label is just a rope @@ -25,7 +25,7 @@ type # this is needed for strange type generation # reasons cfsFieldInfo, # section for field information - cfsTypeInfo, # section for type information + cfsTypeInfo, # section for type information (ag ABI checks) cfsProcHeaders, # section for C procs prototypes cfsData, # section for C constant data cfsVars, # section for C variable declarations @@ -144,6 +144,10 @@ type # without extension) tmpBase*: Rope # base for temp identifier generation typeCache*: TypeCache # cache the generated types + typeABICache*: HashSet[SigHash] # cache for ABI checks; reusing typeCache + # would be ideal but for some reason enums + # don't seem to get cached so it'd generate + # 1 ABI check per occurence in code forwTypeCache*: TypeCache # cache for forward declarations of types declaredThings*: IntSet # things we have declared in this .c file declaredProtos*: IntSet # prototypes we have declared in this .c file diff --git a/compiler/pragmas.nim b/compiler/pragmas.nim index ffe93a9f3..465608b10 100644 --- a/compiler/pragmas.nim +++ b/compiler/pragmas.nim @@ -61,7 +61,7 @@ const wGcSafe, wCodegenDecl} - {wExportNims, wError, wUsed} # why exclude these? typePragmas* = declPragmas + {wMagic, wAcyclic, wPure, wHeader, wCompilerProc, wCore, wFinal, wSize, wShallow, - wIncompleteStruct, wByCopy, wByRef, + wIncompleteStruct, wCompleteStruct, wByCopy, wByRef, wInheritable, wGensym, wInject, wRequiresInit, wUnchecked, wUnion, wPacked, wBorrow, wGcSafe, wPartial, wExplain, wPackage} fieldPragmas* = declPragmas + { @@ -1072,6 +1072,10 @@ proc singlePragma(c: PContext, sym: PSym, n: PNode, i: var int, noVal(c, it) if sym.typ == nil: invalidPragma(c, it) else: incl(sym.typ.flags, tfIncompleteStruct) + of wCompleteStruct: + noVal(c, it) + if sym.typ == nil: invalidPragma(c, it) + else: incl(sym.typ.flags, tfCompleteStruct) of wUnchecked: noVal(c, it) if sym.typ == nil or sym.typ.kind notin {tyArray, tyUncheckedArray}: diff --git a/compiler/sizealignoffsetimpl.nim b/compiler/sizealignoffsetimpl.nim index 81d2f5faf..dc3fefeea 100644 --- a/compiler/sizealignoffsetimpl.nim +++ b/compiler/sizealignoffsetimpl.nim @@ -382,7 +382,8 @@ proc computeSizeAlign(conf: ConfigRef; typ: PType) = computeObjectOffsetsFoldFunction(conf, typ.n, false, accum) let paddingAtEnd = int16(accum.finish()) if typ.sym != nil and - typ.sym.flags * {sfCompilerProc, sfImportc} == {sfImportc}: + typ.sym.flags * {sfCompilerProc, sfImportc} == {sfImportc} and + tfCompleteStruct notin typ.flags: typ.size = szUnknownSize typ.align = szUnknownSize typ.paddingAtEnd = szUnknownSize diff --git a/compiler/types.nim b/compiler/types.nim index c55ecd2aa..eac9695b6 100644 --- a/compiler/types.nim +++ b/compiler/types.nim @@ -22,7 +22,9 @@ type preferGenericArg, preferTypeName, preferResolved, # fully resolved symbols - preferMixed, # show symbol + resolved symbols if it differs, eg: seq[cint{int32}, float] + preferMixed, + # most useful, shows: symbol + resolved symbols if it differs, eg: + # tuple[a: MyInt{int}, b: float] proc typeToString*(typ: PType; prefer: TPreferedDesc = preferName): string template `$`*(typ: PType): string = typeToString(typ) @@ -121,6 +123,13 @@ proc isIntLit*(t: PType): bool {.inline.} = proc isFloatLit*(t: PType): bool {.inline.} = result = t.kind == tyFloat and t.n != nil and t.n.kind == nkFloatLit +proc addDeclaredLoc(result: var string, conf: ConfigRef; sym: PSym) = + result.add " [declared in " & conf$sym.info & "]" + +proc addTypeHeader*(result: var string, conf: ConfigRef; typ: PType; prefer: TPreferedDesc = preferMixed; getDeclarationPath = true) = + result.add typeToString(typ, prefer) + if getDeclarationPath: result.addDeclaredLoc(conf, typ.sym) + proc getProcHeader*(conf: ConfigRef; sym: PSym; prefer: TPreferedDesc = preferName; getDeclarationPath = true): string = assert sym != nil # consider using `skipGenericOwner` to avoid fun2.fun2 when fun2 is generic @@ -140,10 +149,7 @@ proc getProcHeader*(conf: ConfigRef; sym: PSym; prefer: TPreferedDesc = preferNa result.add(')') if n[0].typ != nil: result.add(": " & typeToString(n[0].typ, prefer)) - if getDeclarationPath: - result.add " [declared in " - result.add(conf$sym.info) - result.add "]" + if getDeclarationPath: result.addDeclaredLoc(conf, sym) proc elemType*(t: PType): PType = assert(t != nil) diff --git a/compiler/vmgen.nim b/compiler/vmgen.nim index 5b355bcab..b408db08b 100644 --- a/compiler/vmgen.nim +++ b/compiler/vmgen.nim @@ -726,6 +726,9 @@ proc genBinaryABCD(c: PCtx; n: PNode; dest: var TDest; opc: TOpcode) = c.freeTemp(tmp2) c.freeTemp(tmp3) +template sizeOfLikeMsg(name): string = + "'$1' requires '.importc' types to be '.completeStruct'" % [name] + proc genNarrow(c: PCtx; n: PNode; dest: TDest) = let t = skipTypes(n.typ, abstractVar-{tyTypeDesc}) # uint is uint64 in the VM, we we only need to mask the result for @@ -1317,11 +1320,11 @@ proc genMagic(c: PCtx; n: PNode; dest: var TDest; m: TMagic) = else: globalError(c.config, n.info, "expandToAst requires a call expression") of mSizeOf: - globalError(c.config, n.info, "cannot evaluate 'sizeof' because its type is not defined completely") + globalError(c.config, n.info, sizeOfLikeMsg("sizeof")) of mAlignOf: - globalError(c.config, n.info, "cannot evaluate 'alignof' because its type is not defined completely") + globalError(c.config, n.info, sizeOfLikeMsg("alignof")) of mOffsetOf: - globalError(c.config, n.info, "cannot evaluate 'offsetof' because its type is not defined completely") + globalError(c.config, n.info, sizeOfLikeMsg("offsetof")) of mRunnableExamples: discard "just ignore any call to runnableExamples" of mDestroy: discard "ignore calls to the default destructor" diff --git a/compiler/wordrecg.nim b/compiler/wordrecg.nim index 30a003dee..446897844 100644 --- a/compiler/wordrecg.nim +++ b/compiler/wordrecg.nim @@ -40,7 +40,10 @@ type wImmediate, wConstructor, wDestructor, wDelegator, wOverride, wImportCpp, wImportObjC, wImportCompilerProc, - wImportc, wImportJs, wExportc, wExportCpp, wExportNims, wIncompleteStruct, wRequiresInit, + wImportc, wImportJs, wExportc, wExportCpp, wExportNims, + wIncompleteStruct, # deprecated + wCompleteStruct, + wRequiresInit, wAlign, wNodecl, wPure, wSideEffect, wHeader, wNoSideEffect, wGcSafe, wNoreturn, wNosinks, wMerge, wLib, wDynlib, wCompilerProc, wCore, wProcVar, wBase, wUsed, @@ -128,6 +131,7 @@ const "importcpp", "importobjc", "importcompilerproc", "importc", "importjs", "exportc", "exportcpp", "exportnims", "incompletestruct", + "completestruct", "requiresinit", "align", "nodecl", "pure", "sideeffect", "header", "nosideeffect", "gcsafe", "noreturn", "nosinks", "merge", "lib", "dynlib", "compilerproc", "core", "procvar", "base", "used", @@ -186,6 +190,7 @@ proc canonPragmaSpelling*(w: TSpecialWord): string = of wNoSideEffect: "noSideEffect" of wImportCompilerProc: "importCompilerProc" of wIncompleteStruct: "incompleteStruct" + of wCompleteStruct: "completeStruct" of wRequiresInit: "requiresInit" of wSideEffect: "sideEffect" of wLineDir: "lineDir" diff --git a/doc/nimc.rst b/doc/nimc.rst index 138f1e7f0..7fdceacf5 100644 --- a/doc/nimc.rst +++ b/doc/nimc.rst @@ -438,9 +438,8 @@ Define Effect ``memProfiler`` Enables memory profiling for the native GC. ``uClibc`` Use uClibc instead of libc. (Relevant for Unix-like OSes) ``checkAbi`` When using types from C headers, add checks that compare - what's in the Nim file with what's in the C header - (requires a C compiler with _Static_assert support, like - any C11 compiler) + what's in the Nim file with what's in the C header. + This may become enabled by default in the future. ``tempDir`` This symbol takes a string as its value, like ``--define:tempDir:/some/temp/path`` to override the temporary directory returned by ``os.getTempDir()``. diff --git a/lib/nimbase.h b/lib/nimbase.h index d4fa10050..0b0188b55 100644 --- a/lib/nimbase.h +++ b/lib/nimbase.h @@ -562,10 +562,6 @@ NIM_STATIC_ASSERT(sizeof(NI) == sizeof(void*) && NIM_INTBITS == sizeof(NI)*8, "" # include <sys/types.h> #endif -/* Compile with -d:checkAbi and a sufficiently C11:ish compiler to enable */ -#define NIM_CHECK_SIZE(typ, sz) \ - _Static_assert(sizeof(typ) == sz, "Nim & C disagree on type size") - /* these exist to make the codegen logic simpler */ #define nimModInt(a, b, res) (((*res) = (a) % (b)), 0) #define nimModInt64(a, b, res) (((*res) = (a) % (b)), 0) diff --git a/tests/misc/msizeof5.nim b/tests/misc/msizeof5.nim new file mode 100644 index 000000000..c833d1e43 --- /dev/null +++ b/tests/misc/msizeof5.nim @@ -0,0 +1,131 @@ +## tests for -d:checkAbi used by addAbiCheck via NIM_STATIC_ASSERT + +{.emit:"""/*TYPESECTION*/ +struct Foo1{ + int a; +}; +struct Foo2{ + int a; +}; +enum Foo3{k1, k2}; +typedef enum Foo3 Foo3b; +typedef enum Foo4{k3, k4} Foo4; + +typedef int Foo5[3]; + +typedef struct Foo6{ + int a1; + bool a2; + double a3; + struct Foo6* a4; +} Foo6; +""".} + +template ensureCgen(T: typedesc) = + ## ensures cgen + var a {.volatile.}: T + +block: + type Foo1Alias{.importc: "struct Foo1", size: sizeof(cint).} = object + a: cint + ensureCgen Foo1Alias + +block: + type Foo3Alias{.importc: "enum Foo3", size: sizeof(cint).} = enum + k1, k2 + ensureCgen Foo3Alias + +block: + type Foo3bAlias{.importc: "Foo3b", size: sizeof(cint).} = enum + k1, k2 + ensureCgen Foo3bAlias + +block: + type Foo3b{.importc, size: sizeof(cint).} = enum + k1, k2 + ensureCgen Foo3b + static: + doAssert Foo3b.sizeof == cint.sizeof + +block: + type Foo4{.importc, size: sizeof(cint).} = enum + k3, k4 + # adding entries should not yield duplicate ABI checks, as enforced by + # `typeABICache`. + # Currently the test doesn't check for this but you can inspect the cgen'd file + ensureCgen Foo4 + ensureCgen Foo4 + ensureCgen Foo4 + +block: + type Foo5{.importc.} = array[3, cint] + ensureCgen Foo5 + +block: + type Foo5{.importc.} = array[3, cint] + ensureCgen Foo5 + +block: # CT sizeof + type Foo6GT = object # grountruth + a1: cint + a2: bool + a3: cfloat + a4: ptr Foo6GT + + type Foo6{.importc, completeStruct.} = object + a1: cint + a2: bool + a3: cfloat + a4: ptr Foo6 + + static: doAssert compiles(static(Foo6.sizeof)) + static: doAssert Foo6.sizeof == Foo6GT.sizeof + static: doAssert (Foo6, int, array[2, Foo6]).sizeof == + (Foo6GT, int, array[2, Foo6GT]).sizeof + +block: + type GoodImportcType {.importc: "signed char", nodecl.} = char + # "good" in sense the sizeof will match + ensureCgen GoodImportcType + +block: + type Foo6{.importc.} = object + a1: cint + doAssert compiles(Foo6.sizeof) + static: doAssert not compiles(static(Foo6.sizeof)) + +when defined caseBad: + # Each case below should give a static cgen assert fail message + + block: + type BadImportcType {.importc: "unsigned char", nodecl.} = uint64 + # "sizeof" check will fail + ensureCgen BadImportcType + + block: + type Foo2AliasBad{.importc: "struct Foo2", size: 1.} = object + a: cint + ensureCgen Foo2AliasBad + + block: + type Foo5{.importc.} = array[4, cint] + ensureCgen Foo5 + + block: + type Foo5{.importc.} = array[3, bool] + ensureCgen Foo5 + + block: + type Foo6{.importc, completeStruct.} = object + a1: cint + # a2: bool # missing this should trigger assert fail + a3: cfloat + a4: ptr Foo6 + ensureCgen Foo6 + + when false: + block: + # pre-existing BUG: this should give a CT error in semcheck because `size` + # disagrees with `array[3, cint]` + type Foo5{.importc, size: 1.} = array[3, cint] + ensureCgen Foo5 diff --git a/tests/misc/tsizeof2.nim b/tests/misc/tsizeof2.nim index a193cf7c3..da28de508 100644 --- a/tests/misc/tsizeof2.nim +++ b/tests/misc/tsizeof2.nim @@ -1,5 +1,5 @@ discard """ -errormsg: "cannot evaluate 'sizeof' because its type is not defined completely" +errormsg: "'sizeof' requires '.importc' types to be '.completeStruct'" line: 9 """ diff --git a/tests/trunner.nim b/tests/trunner.nim index 7e1468e84..263184571 100644 --- a/tests/trunner.nim +++ b/tests/trunner.nim @@ -19,14 +19,10 @@ proc runCmd(file, options = ""): auto = echo result[0] echo result[1] -proc testCodegenStaticAssert() = - let (output, exitCode) = runCmd("ccgbugs/mstatic_assert.nim") - doAssert "sizeof(bool) == 2" in output - doAssert exitCode != 0 - -proc testCTFFI() = - let (output, exitCode) = runCmd("vm/mevalffi.nim", "--experimental:compiletimeFFI") - let expected = """ +when defined(nimHasLibFFIEnabled): + block: # mevalffi + let (output, exitCode) = runCmd("vm/mevalffi.nim", "--experimental:compiletimeFFI") + let expected = """ hello world stderr hi stderr foo @@ -37,10 +33,30 @@ foo:102:103:104 foo:0.03:asdf:103:105 ret={s1:foobar s2:foobar age:25 pi:3.14} """ - doAssert output == expected, output - doAssert exitCode == 0 + doAssert output == expected, output + doAssert exitCode == 0 -when defined(nimHasLibFFIEnabled): - testCTFFI() else: # don't run twice the same test - testCodegenStaticAssert() + template check(msg) = doAssert msg in output, output + + block: # mstatic_assert + let (output, exitCode) = runCmd("ccgbugs/mstatic_assert.nim", "-d:caseBad") + check "sizeof(bool) == 2" + doAssert exitCode != 0 + + block: # ABI checks + let file = "misc/msizeof5.nim" + block: + let (output, exitCode) = runCmd(file, "-d:checkAbi") + doAssert exitCode == 0, output + block: + let (output, exitCode) = runCmd(file, "-d:checkAbi -d:caseBad") + # on platforms that support _StaticAssert natively, errors will show full context, eg: + # error: static_assert failed due to requirement 'sizeof(unsigned char) == 8' + # "backend & Nim disagree on size for: BadImportcType{int64} [declared in mabi_check.nim(1, 6)]" + check "sizeof(unsigned char) == 8" + check "sizeof(struct Foo2) == 1" + check "sizeof(Foo5) == 16" + check "sizeof(Foo5) == 3" + check "sizeof(struct Foo6) == " + doAssert exitCode != 0 |