hsutter / cppfront Goto Github PK
View Code? Open in Web Editor NEWA personal experimental C++ Syntax 2 -> Syntax 1 compiler
License: Other
A personal experimental C++ Syntax 2 -> Syntax 1 compiler
License: Other
Having a single expression function that uses multiple return values syntax compiles to bad cpp1 code.
fun: () -> (i:int) = i = 42;
tests % cppfront ufcs.cpp2
ufcs.cpp2... ok (all Cpp2, passes safety checks)
tests % clang++ -std=c++20 -I../cppfront/include ufcs.cpp
tests/ufcs.cpp2:24:47: error: use of undeclared identifier 'i'
[[nodiscard]] auto fun() -> fun__ret { return i.construct(42); }
^
1 error generated.
It should fail on cppfront or compile to correct code like:
[[nodiscard]] auto fun() -> fun__ret{
cpp2::deferred_init<int> i;
i.construct(42);
return { std::move(i.value()) };
}
cppfront generates bad code that fails to compile:
[[nodiscard]] auto fun() -> fun__ret { return i.construct(42); }
Compiler version
tests % clang++ --version
Apple clang version 14.0.0 (clang-1400.0.29.102)
Target: arm64-apple-darwin21.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
C# syntax is already a millions of developers familiar with syntax, defiantly there are few differences, but I think it worth that one day millions of c# developers just found that they are able to code in C++
Not sure if you want these types of discussions here...but here goes nothing. I really like a lot about this effort. However, unless I'm missing something, it does not address the problem of ABI breaks in C++. Now, fixing many problems is better than fixing no problems...but it seems like a solution that will (paraphrasing) "keep C++ relevant for the next 30 years" needs to solve that problem. Will cpp2 introduce anything to address that...or does it ignore it?
Specialisation of cpp2::to_string
function for std::optional
is using std::to_string
on optional value.
template<typename T>
auto to_string(std::optional<T> const& o) -> std::string {
if (o.has_value()) {
return std::to_string(o.value());
}
return "(empty)";
}
That makes the below code fail to compile:
v : std::optional<std::string> = "this will not work";
std::court << "(v)$" << std::endl;
cppfront will generate
std::optional<std::string> v { "this will not work" };
std::cout << "My map: " + cpp2::to_string(v) + "\n" << std::endl;
The result is that there is a call to std::to_string(std::string)
and there is no such specialization in the standard library.
Raw pointers are fundamentally problematic if we have to do pointer arithmetic or offset based access. A major source for pointers, that we inherited from C, are strings. There are safer alternatives to C strings (i.e., string_view), but they are not used by default.
We still need classic C strings for compatibility reasons, but the default should be a safe construct.
Thus, I would suggest that a string literal in cpp2 mode is considered a string_view by default. Traditional C strings could be constructed by using, e.g., the c suffix:
cpp2: "abc" -> cpp: "abc"sv
cpp2: "abc"c -> cpp: "abc"
That would make string handling much cleaner and safer. Compatibility with existing code is a concern, but as cpp2 code is by definition new anyway, we can always add the 'c' suffix if needed to get a traditional C string.
Sorry to be that guy but the whole "experiment" with new c++ syntax is rather pointless.
We already have that: It is called Rust and does everything and more than cppfront2 is trying to poorly mimic and imitate.
Also, one of main selling points for rust like explicit lifetimes controlled by the compiler - this is simply not gonna happen in cpp. So basically Herb is trying to create/promote knock-off technology that is already outdated...
Just a suggestion to consider enabling the GitHub Discussions feature on this cppfront repo.
A stated project aim is to start a discussion, this could be a quick way to kickstart things. It could be convenient for now. The Issues tab has a different connotation and feels a barrier to contributions that aim at a collaborative conversation.
(I don't have much experience of Discussions pros and cons.)
While I do understand and support the drive to avoid using unsafe features such as raw unions, pointer arithmetic, unsafe casts, etc. It is sometimes necessary to use these features, for example, when interfacing with low-level code, hardware, or simply implementing a higher-level structure (ex. a custom memory allocator or a hash table). For example, you may want to store trivial CPU-specific types in a union for intrinsics. While there are ways to replace unions and pointer arithmetic with variant
and friends, in these low-level tightly controlled situations they either are not applicable, or will require more overhead than might be necessary.
For these situations, you would have to implement your low-level code in syntax1, then use it in you syntax2 code. This might become very tedious and complicated, especially when you need to use the unsafe code in a header, you'd have to split your implementation in syntax1/syntax2 headers and would have something akin to my_table.hpp2
and my_table.hpp
, with the user only being allowed to include the hpp2
file. Which, i think you'd aggree would just complicate design and maintnance.
As such, why not enable the developer to explicitly allow unsafe code in certain situations, such as via an unsafe
keyword applied to a structures, functions, variables or code blocks. Code and data tagged as unsafe
would then be allowed to use unions, pointer arithmetic, and other low-level unsafe features, without having to separate safe/unsafe code among multiple files.
This way you can implement your embeded binary, hardware driver, custom heap allocator and whatever need be in syntax2 files, while using "modern" and safe code, and only drop down to unsafe context in very few situations with surgical precision.
Hi,
I tried to build this awesome project, but got disappointing error. Would you be able to help me?
cppfront
❯ clang source/cppfront.cpp -std=c++20 -o cppfront
In file included from source/cppfront.cpp:18:
In file included from source/sema.h:21:
source/parse.h:150:5: error: use of class template 'String' requires template arguments; argument deduction not allowed in template parameter
String Name,
^~~~~~
source/common.h:211:8: note: template is declared here
struct String
^
In file included from source/cppfront.cpp:18:
In file included from source/sema.h:21:
source/parse.h:201:64: error: value of type 'const char [6]' is not implicitly convertible to 'int'
using is_as_expression_node = binary_expression_node< "is-as" , prefix_expression_node >;
^~~~~~~
source/parse.h:202:83: error: unknown type name 'is_as_expression_node'; did you mean 'id_expression_node'?
using multiplicative_expression_node = binary_expression_node< "multiplicative" , is_as_expression_node >;
^~~~~~~~~~~~~~~~~~~~~
id_expression_node
source/parse.h:110:8: note: 'id_expression_node' declared here
struct id_expression_node;
^
source/parse.h:202:64: error: value of type 'const char [15]' is not implicitly convertible to 'int'
using multiplicative_expression_node = binary_expression_node< "multiplicative" , is_as_expression_node >;
^~~~~~~~~~~~~~~~
source/parse.h:203:83: error: use of undeclared identifier 'multiplicative_expression_node'
using additive_expression_node = binary_expression_node< "additive" , multiplicative_expression_node >;
^
source/parse.h:204:83: error: use of undeclared identifier 'additive_expression_node'
using shift_expression_node = binary_expression_node< "shift" , additive_expression_node >;
^
source/parse.h:205:83: error: unknown type name 'shift_expression_node'; did you mean 'id_expression_node'?
using compare_expression_node = binary_expression_node< "compare" , shift_expression_node >;
^~~~~~~~~~~~~~~~~~~~~
id_expression_node
source/parse.h:110:8: note: 'id_expression_node' declared here
struct id_expression_node;
^
source/parse.h:205:64: error: value of type 'const char [8]' is not implicitly convertible to 'int'
using compare_expression_node = binary_expression_node< "compare" , shift_expression_node >;
^~~~~~~~~
source/parse.h:206:83: error: use of undeclared identifier 'compare_expression_node'
using relational_expression_node = binary_expression_node< "relational" , compare_expression_node >;
^
source/parse.h:207:83: error: use of undeclared identifier 'relational_expression_node'
using equality_expression_node = binary_expression_node< "equality" , relational_expression_node >;
^
source/parse.h:208:83: error: use of undeclared identifier 'equality_expression_node'; did you mean 'binary_expression_node'?
using bit_and_expression_node = binary_expression_node< "bit-and" , equality_expression_node >;
^~~~~~~~~~~~~~~~~~~~~~~~
binary_expression_node
source/parse.h:153:8: note: 'binary_expression_node' declared here
struct binary_expression_node
^
source/parse.h:208:83: error: use of undeclared identifier 'equality_expression_node'
using bit_and_expression_node = binary_expression_node< "bit-and" , equality_expression_node >;
^
source/parse.h:209:83: error: use of undeclared identifier 'bit_and_expression_node'; did you mean 'binary_expression_node'?
using bit_xor_expression_node = binary_expression_node< "bit-xor" , bit_and_expression_node >;
^~~~~~~~~~~~~~~~~~~~~~~
binary_expression_node
source/parse.h:153:8: note: 'binary_expression_node' declared here
struct binary_expression_node
^
source/parse.h:209:83: error: use of undeclared identifier 'bit_and_expression_node'
using bit_xor_expression_node = binary_expression_node< "bit-xor" , bit_and_expression_node >;
^
source/parse.h:210:83: error: use of undeclared identifier 'bit_xor_expression_node'; did you mean 'binary_expression_node'?
using bit_or_expression_node = binary_expression_node< "bit-or" , bit_xor_expression_node >;
^~~~~~~~~~~~~~~~~~~~~~~
binary_expression_node
source/parse.h:153:8: note: 'binary_expression_node' declared here
struct binary_expression_node
^
source/parse.h:210:83: error: use of undeclared identifier 'bit_xor_expression_node'
using bit_or_expression_node = binary_expression_node< "bit-or" , bit_xor_expression_node >;
^
source/parse.h:211:83: error: use of undeclared identifier 'bit_or_expression_node'; did you mean 'binary_expression_node'?
using logical_and_expression_node = binary_expression_node< "logical-and" , bit_or_expression_node >;
^~~~~~~~~~~~~~~~~~~~~~
binary_expression_node
source/parse.h:153:8: note: 'binary_expression_node' declared here
struct binary_expression_node
^
source/parse.h:211:83: error: use of undeclared identifier 'bit_or_expression_node'
using logical_and_expression_node = binary_expression_node< "logical-and" , bit_or_expression_node >;
^
source/parse.h:212:83: error: use of undeclared identifier 'logical_and_expression_node'
using logical_or_expression_node = binary_expression_node< "logical-or" , logical_and_expression_node >;
^
fatal error: too many errors emitted, stopping now [-ferror-limit=]
20 errors generated.
clang
❯ clang --version
Apple clang version 12.0.5 (clang-1205.0.22.11)
Target: x86_64-apple-darwin20.4.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
@hsutter regarding your question:
Line 809 in bf5998a
I am currently trying to use cppfront to build my project (that's why I send PRs - I am implementing what I am missing).
I use something like your c_raii
(I call it scope_exit
and I have a function on_scope_exit
that produces it) for handling popen
(here I am using UFCS chaining from here: #18):
execute: (cmd : std::string) -> auto = {
std_out := popen(cmd.c_str(), "r").on_scope_exit(pclose);
buf : std::array<char, 1024> = ();
output : std::string = ();
read_size := fread(buf.begin(), 1, buf.size(), std_out);
while read_size > 0 next read_size = fread(buf.begin(), 1, buf.size(), std_out) {
output += std::string(buf.begin(), read_size);
}
return output;
}
and to remove temporary directories on scope exit
create_temporary_directory: () -> auto = {
tmp := fs::temp_directory_path();
build_dir := on_scope_exit(tmp / "md_tests/build", :(p : fs::path) = {
fs::remove_all(p);
std::cout << "removed tmp directory: (p)$" << std::endl;
} );
fs::create_directories(build_dir);
std::cout << "created tmp directory: (fs::path(build_dir))$" << std::endl;
return build_dir;
}
My current implementation of on_scope_exit()
:
template <typename T, typename D>
struct scope_exit {
scope_exit(T v, D d)
: value(v)
, deleter(d)
{
}
~scope_exit() {
deleter(value);
}
operator T&() { return value; }
private:
T value;
D deleter;
};
on_scope_exit: (forward v : _, forward d : _) -> auto = {
return scope_exit(v,d);
}
I will check your implementation and will let you know.
Describe the bug
Two expected outputs are stale
mixed-function-expression-with-pointer-capture
mixed-test-parens
To Reproduce
See the failing workflow run here.
Additional context
The workflow doesn't show the diff of the failure, yet, unfortunately.
How do you forward a non-last-use argument? It has to be done when destructuring aggregates. std::apply does this variadically.
#include <string>
void g(auto&& x);
struct pair_t {
std::string x, y;
};
f: (forward pair : pair_t) = {
g(pair.x); // Not a forward. How do I forward?
g(pair.y); // OK
}
I want to use namespaces in the cpp2. Do you have any plan for syntax that will be used for it?
I know that the code is identified as cpp2 by
// Switch to cpp2 mode if we're not in a comment, not inside nested { },
// and the line starts with "nonwhitespace :" but not "::"
Based on my current experience with cppfront I try:
N : namespace = {
fun: (i : _) -> auto = { return i * 10; }
}
it produces
namespace N { {
[[nodiscard]] auto fun(auto const& i) -> auto{return i * 10; }
} };
close enough - too many braces. (https://godbolt.org/z/f61h9W8Mn)
To my surprise, this worked
N : namespace = fun: (i : _) -> auto = { return i * 10; }
and produces
namespace N { [[nodiscard]] auto fun(auto const& i) -> auto{return i * 10; } };
(https://godbolt.org/z/nWWbs7d3K)
In lower_to_cpp1()
it accidentally matches
// Object with optional initializer
else if (!printer.doing_declarations_only() && n.is(declaration_node::object))
the fun part is that using the same pattern we can define a struct
or a class
N : struct = i:int = 42;
that produces
struct N { int i { 42 }; };
Voilà! cppfront has class support ;) (https://godbolt.org/z/89aPas1nr)
Probably the syntax should be
N : namespace = {
fun: (i : _) -> auto = { return i * 10; }
}
What do you think?
I am trying to introduce the change in the cppfront that enables it but I need to spend some time to understand how it works to be able to spot the right place.
PS: I didn't know what type of issue to select - sorry for making a mess.
Have you considered making variables const by default? I have seen it float around as a recommendation and personally am of the "const everything" camp. It encourages more careful code, catches many errors and makes it more readable (if I see same variable later down the line, I know it has the same value, don't need to check every line in between).
a: int = 3;
a = 5; // error! implicit const
b: mutable int = 3;
b = 5; // fine
On slide 74 in https://github.com/CppCon/CppCon2022/blob/main/Presentations/CppCon-2022-Sutter.pdf we have the example of initialization safety by naming the return values already in the definition
f:() -> (i: int, s: std::string) = {
i = 1;
s = "x";
return;
}
We basically know this function returns i
and s
- and we guarantee initialization in the code with Cpp2 anyway.
Can't we just omit the empty return;
then? What is the value of having to write return
?
main : () -> int = {
i : const int = 0;
s : const std::string = "Hello, world!";
}
gets turned into
[[nodiscard]] auto main() -> int{
int const i { 0 };
std const::string s { "Hello, world!" };
}
Exclamation signs disappear when compiled by cppfront.
Cpp2 code:
main: () -> int = {
b : bool = false;
c := !b;
while !b || !is_true() {
b = !true;
}
}
is_true: () -> bool = false;
after compilation with cppfront (I will limit the code to line that matters)
[[nodiscard]] auto main() -> int{
bool b { false };
auto c { b };
while( b || is_true() ) {
b = true;
}
}
All exclamation signs disappeared - no way to negate the boolean expression.
Expected result:
[[nodiscard]] auto main() -> int{
bool b { false };
auto c { !b };
while( !b || !is_true() ) {
b = !true;
}
}
The parser eagerly turns *
into postfix operators, which is not always correct. Consider the example below:
foo : (a:int) -> auto = { return a*2; }
It fails to compile in cppfront with an error message (missing ';'
). The error goes away when adding a space in front of the *
because the parser then does not recognize it as postfix operator. But insisting on a whitespace in front on a binary operator seems questionable, it is a significant deviation from regular C++.
In general the postfix *
and &
operators are problematic, as they create an ambiguity with binary *
and &
. Resolving that requires either an LR(2) parser or a lot of lexer magic to recognize these as postfix operators. Having them as prefix operators would be easier.
There are also other ambiguities in the grammar, some of them probably fixable (e.g., template-argument -> expression | id-expression
, but if the expression is an identifier the parser cannot distinguish these cases), some of them probably unfixable without major changes to the language (e.g., template syntax, there the parser cannot parse a<b,c>d
correctly without knowing if a
is a template or not). I am not sure if it makes sense to open bugs for all of them. If you want I can report them, of course, but I do not want to spam the issue tracker with problems if you only consider the syntax experimental anyway. (On the other hand it is probably useful to know if the suggested syntax works or not).
After introducing output
option the #line
directives points to the output file path instead of cpp2 source path.
i : int = 42;
1. build/external/cppfront external/tests/easy.cpp2 -o build/easy.cpp
build/easy.cpp
content
// ----- Cpp2 support -----
#include "cpp2util.h"
#line 1 "build/easy.cpp2"
//=== Cpp2 definitions ==========================================================
#line 1 "build/easy.cpp2"
int i { 42 };
#line
directive points to file build/easy.cpp2
that does not exists. The expected path is external/tests/easy.cpp2
Expected file content:
// ----- Cpp2 support -----
#include "cpp2util.h"
#line 1 "external/tests/easy.cpp2"
//=== Cpp2 definitions ==========================================================
#line 1 "external/tests/easy.cpp2"
int i { 42 };
The issue is that the last use detection of variables doesn't take into account loops. If the last use is detected inside a loop body it will apply std::move()
and it will be consumed on the first iteration.
wreakit: (copy s: std::string) -> std::string = {
wreak: std::string = "it";
i: int = 0;
while i < 2 next i++ {
wreak = s;
}
return wreak;
}
Generates the code:
[[nodiscard]] auto wreakit(std::string s) -> std::string{
std::string wreak { "it" };
int i { 0 };
for( ; i < 2; ++i ) {
wreak = std::move(s);
}
return wreak;
}
Expected value returned by wreakit("ralph");
would be "ralph"
but actual value is ""
;
I know that class are still not supported on cppfront; but this makes the discussion even more relevant.
One of the things that lambda functions (arguably) get right is making their operator()
const
by default and requiring mutable
if you want to change data from the lambda.
This should be done also for cpp2 classes. This has the advantages of (1) making lambdas less of a special case, and (2) making const-correct code the default.
This is kind of related to issue #25 .
So based on what I understand about cppfront and Cpp2 as it stands, there's a union
keyword that effectively infers / uses an std::variant
wherever it's used, and it's invisible to the end-user-- I think that's great and a good choice.
However, it looks like when using optionals, you still have to declare them with the original C++ syntax… I think there's a missed opportunity here that has been well treaded in C# (via "Nullables"), JavaScript / TypeScript, and Swift.
If you look at Swift's Optionals, we have declaration like this:
var a: Int // int type
var b: Int? // Optional Int type
var c: Dog? // Optional class type
And accessing values are like this (the last two lines are called "optional chaining" in most languages):
print(b ?? 7) // prints the value of "b" if non-optional, otherwise prints "7"
let h = c?.height // accessing member variables from an optional class
c?.bark() // calls "bark" method on c if c is not null, does nothing otherwise
d?.data?.dog?.bark() // Similar to above, but you have a hierarchy of optional members
Swift also provides the concept of "unwrapping" an optional; I don't want to muck up the above with this, but it would be negligent of me not to mention it:
if var c = c { // try to "unwrap" the optional into a non-optional type
c.bark() // optional was successfully "unwrapped", c is now a non-optional type, and "bark" method can be called
}
In my opinion, Swift is a very "Optional first" type of language, meaning you often want to see if something "can be" an optional before you even consider a non-optional type, and I think this kind of thinking might also benefit C++… and maybe a good vector to do this would be through Cpp2 / cppfront.
If for nothing else the syntax sugar of using a declaration of type?
to imply std::optional<type>
and providing support for optional chaining (the c?.bark()
instead of writing if (*c) { c->bark(); }
would be tremendous in my opinion.
One other thing to consider about Optionals in general (as well as how they currently stand in Cpp2), is that the absence of a value is typically represented as null
or nil
or (in the case of C++)nullopt
. I do agree (and totally support) the idea of eliminating NULL
in Cpp2, but I think they track you'll find here is similar to Swift, where nil
(their version of null
) only exists to support the "absense of a value in an optional."
Whether or not you wanted to go down a similar "unwrap" path would be up to you (and dependent on if you even wanted to go down this "syntax sugar" idea or not), but I think there's lots to learn from other languages that do optionals that C++ can't do without considerable effort that are worth considering here.
tl;dr:
union
-> std::variant
b: Int?
c?.bark()
b ?? 5
nullopt_t
is okay, but maybe there's a better long-term naming solution)Currently cppfront
seems to write its output .cpp
file next to the input .cpp2
file. This makes it difficult to integrate with setups that have separate build and source directories. Please add a -o
command line argument so the output path can be specified from the outside.
Out of curiosity I have an implemented an alternative parser for cppfront / cpp2, which uses a PEG grammar as input for a parser generator. During that experiment, I noticed that the grammar rules embedded as //G
comments are not always correct. I will list errors that I noticed below.
One preliminary note: The cppfront compiler has a rather relaxed concept of keywords. In most cases it will accept a keyword were an identifier is expected, for example it will happily compile if: () -> void = { }
. I don't think that is a good idea, my grammar explicitly distinguishes between keywords and identifiers. (Modulo the few context specific soft-keywords like in
/out
etc.). For some grammar rules that requires changes were the parser previously worked by accident (i.e, by not recognizing a certain keyword).
a) id_expression
//G id-expression
//G unqualified-id
//G qualified-id
//G
here the order is wrong, it should be
//G id-expression
//G qualified-id
//G unqualified-id
//G
b) primary_expression
//G primary-expression:
//G literal
//G ( expression-list )
//G id-expression
//G unnamed-declaration
//G inspect-expression
//G
this does not correspond to the source code order. Furthermore, the expression-list is optional. And if we distinguish keywords from literals we potentially need some extra rules to handle keywords that are currently silently eaten as identifier. I would suggest
//G primary-expression:
//G inspect-expression
//G id-expression
//G literal
//G '(' expression-list? ')'
//G unnamed-declaration
//G 'nullptr'
//G 'true'
//G 'false'
//G 'typeid' '(' expression ')'
//G 'new' < id-expression > '(' expression-list? ')'
c) nested-name-specifier
//G nested-name-specifier:
//G ::
//G unqualified-id ::
this has to support nested scopes. I would suggest
//G nested-name-specifier:
//G :: (unqualified-id ::)*
//G (unqualified-id ::)+
d) template-argument
//G template-argument:
//G expression
//G id-expression
There should be a comment here that we disable '<'/'>'/'<<'/'>>' in the expressions until a new parentheses is opened. In fact that causes some of the expression rules to be cloned until we reach the level below these operators. (In my implementation these are the rules with suffix _no_cmp).
e) id-expression from fundamental types
We want to accept builtin types like int
as type ids. Currently this works by accident because the parser does not even recognize these as keywords. When enforcing that keywords are not identifiers we need rules for these, too. I have added a fundamental-type
alternative at the end of id-expression, and have defines that as follows:
fundamental-type
'void'
fundamental-type-modifier_list? 'char'
'char8_t'
'char16_t'
'char32_t'
'wchar_t'
fundamental-type-modifier-list? 'int'
'bool'
'float'
'double'
'long' 'double'
fundamental-type-modifier-list
fundamental-type-modifier-list
fundamental-type-modifier+
fundamental-type-modifier
'unsigned'
'signed'
'long'
'short'
I know that namespaces are not supported yet by cppfront. Unfortunately, it can be added following the l-to-r approach, it compiles by cppfront but fails to compile by cpp1 compiler.
N: namespace = fun: () -> (a:int) = {
a = 42;
return;
}
Cppfront compiles fine but generates code that will not compile as it doesn't have a struct definition for multiple return values.
// ----- Cpp2 support -----
#include "cpp2util.h"
//=== Cpp2 definitions ==========================================================
namespace N { [[nodiscard]] auto fun() -> fun__ret{
cpp2::deferred_init<int> a;
a.construct(42);
return { std::move(a.value()) };
} };
https://godbolt.org/z/vr51TExGG
No definition of fun__ret
.
Expected behavior: cppfront forbids namespaces/struct/class to be used like that or generates correct code.
I have a bit of a concern about the postfix type
and namespace
keywords, both about consistency and implementation caveats.
The main gist of my concern is that with postfix type
keyword, a type's parents cannot be defined as they are in syntax 1, as column is being used for the keyword.
Of course it can be done via Java-style extends
(or similar) keyword. Or via a second column after the type
. But that just seems like introducing visual noise into the syntax for the sake of having a postfix keyword.
Another concern is how would template declarations and concept requirements work, would the template/requires keywords also come after the column like this?
my_type : template<typename T> requires (X) type
or like this
my_type : type template<typename T> requires (X)
This, at least to me, seems a bit noisy as well.
And finally - consistency: this way there will be prefix keywords (i.e. if
, for
, etc.), postfix keywords without a column (ex. noexcept
) and postfix keywords with a column (type
). This, again, seems to me like additional syntactical noise for the sake of "using the shiny postfix".
Are there any valid reasons for the postfix keywords for types/namespaces that i just dont see? How would inheritance, templates, etc. work with the postfix keywords?
CPP2_UFCS macro is based on variadic macros. That means that every comma ,
in the macro separates two arguments for the macro (unless surrounded by parentheses ()
.
That makes below the cpp2 code (mixed mode):
#include <string>
template <auto from, auto to>
auto substr(const std::string& input) -> std::string {
return input.substr(from, to-from);
}
main: () -> int = {
test_string: std::string = "Unfortunatelly macros have limitations";
cmd.substr<3,6>();
}
compiles to (skipping boilerplate):
[[nodiscard]] auto main() -> int{
std::string test_string { "Unfortunatelly macros have limitations" };
CPP2_UFCS_0(substr<3,6>, test_string);
}
And a call to CPP2_UFCS_0
will be perceived by the compiler as CPP2_UFCS_0((substr<3),(6>), (test_string));
(parentheses added to emphasize the problem. That will not compile and will end up with an error:
clang++ -std=c++20 -Iinclude ../tests/ufcs-method-limitations.cpp
tests/ufcs-method-limitations.cpp2:10:30: error: too many arguments provided to function-like macro invocation
CPP2_UFCS_0(substr<3,6>, test_string);
^
include/cpp2util.h:487:9: note: macro 'CPP2_UFCS_0' defined here
#define CPP2_UFCS_0(FUNCNAME,PARAM1) \
^
tests/ufcs-method-limitations.cpp2:10:5: error: use of undeclared identifier 'CPP2_UFCS_0'
CPP2_UFCS_0(substr<3,6>, test_string);
^
2 errors generated.
A similar error will happen for CPP2_UFCS
version - that reminds me why we hate macros so much.
Normally, in such cases, we would add parentheses to give hints to the preprocessor where are the arguments. In this case when we do it we will look like:
CPP2_UFCS_0((substr<3,6>), test_string);
But it will also fail to compile with the error: expected unqualified-id
:
clang++ -std=c++20 -Iinclude ../tests/ufcs-method-limitations.cpp
tests/ufcs-method-limitations.cpp2:10:17: error: expected unqualified-id
CPP2_UFCS_0((substr<3,6>), test_string);
^
tests/ufcs-method-limitations.cpp2:10:17: error: expected unqualified-id
error: constexpr if condition is not a constant expression
tests/ufcs-method-limitations.cpp2:10:5: note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<std::string &>' requested here
CPP2_UFCS_0((substr<3,6>), test_string);
^
include/cpp2util.h:494:2: note: expanded from macro 'CPP2_UFCS_0'
}(PARAM1)
^
3 errors generated.
Unless there is a trick to make that macro work again we will need to come up with some other idea to solve it.
I found that bug while trying to update the function call chaining PR (#18). Probably working UFCS will not be possible without semantic analysis on cppfront side.
The same will be needed to allow Unified "." member/scope selection (e.g., std.swap(x,y))
- it is impossible to distinguish if std
is a namespace by using require
expressions or decltype
hacks (already tried that).
Something I haven't seen addressed or talked about is how cppfront
or the new cpp2 syntax will handle type casting. When I write code, I try to be very explicit and specific with my types and that includes adding a ton of casts. I also do thing such as turn on -pendaic
and -Werror
to catch any type casts that I might have missed to do.
Is there any proposal to make casting easier and simpler?
E.g., I've never understood why I might get a warning/error from a compiler if I do this:
int32_t a = 0;
int64_t b = a;
I can understand the cast from 64 -> 32 bits, since there is a loss in precision. But not from going 32 -> 64 bits since there is a growth in the precision.
Talking about the syntax for the moment, I've also had my share of colleagues who don't like writing out static_cast<>
because they thing "It's too long to type out when just a simple C-style cast suffices.". Is there a syntactical change that could be make to make proper casting easier?. E.g. reduce static_cast<>
to scast<>
?
I'm also not sure if the names of static_cast
, dynamic_cast
, reinterpret_cast
and const_cast
are that great, as when I first was learning C++ a decade+ ago I had trouble trying to understand the meaning of the assigned names.
Parens disappear when used inside function call to calculate value of argument.
Given cpp2 code:
f: (x:_) = {}
main: () -> int = {
i := 2;
f((i+(i+1)*2)/2);
}
When compiled with cppfront we get (I am skipping the boilerplate)
[[nodiscard]] auto main() -> int{
auto i { 2 };
f( i + i + 1 * 2 / 2);
}
all parens inside the function call are gone. (i+(i+1)*2)/2
is not equal i + i + 1 * 2 / 2
.
The expectation is that all parens used to calculate arguments will last cppfront compilation.
Hi, I am very interested in this project's future!
Cppfront fails to compile this code
main: () -> int = {
a: int = 2;
pa: *int = a&;
ppa: **int = pa&;
return a*pa**ppa**; // 8
}
As of now, ppa: **int = pa&;
does not work.
The local variables can be initialized by external functions that take out
argument. cpp2::deferred_init
treat it as violation of initialization safety and reports error:
error: local variable i is used before it was initialized
fun: () -> (i:int) = {
init(i);
return;
}
init: (out i : int) = {
i = 42;
}
After trying to compile that with cppfront we got:
cppfront % build/external/cppfront external/tests/defered.cpp2
external/tests/defered.cpp2...
defered.cpp2(2,10): error: local variable i is used before it was initialized
==> program violates initialization safety guarantee - see previous errors
I expect cppfront accepts the functions that have out
argument as proper initialization of deferred_init
variables (assuming that there is a check that out
argument, inside the function, has an assignment operation done on it).
The current implementation is not supporting enums in inspect expressions.
This cpp2 code
enum class lexeme : std::uint8_t {
hash,
};
to_string: (e:lexeme) -> auto = {
return inspect (e) -> std::string {
is lexeme::hash = "hash";
is _ = "INTERNAL_ERROR";
};
}
compiles by cppfront to (skipping enum declaration):
[[nodiscard]] auto to_string(cpp2::in<lexeme> e) -> auto{
return [&] () -> std::string { auto&& __expr = (e);
if (cpp2::is<lexeme::hash>(__expr)) { if constexpr( requires{"hash";} ) if constexpr( std::is_convertible_v<CPP2_TYPEOF("hash"),std::string> ) return "hash"; else return std::string{}; else return std::string{}; }
else return "INTERNAL_ERROR"; }
()
; }
and fails to compile with the cpp1 compiler with the error:
main.cpp2:26:13: error: no matching function for call to 'is'
[build] if (cpp2::is<lexeme::hash>(__expr)) { if constexpr( requires{"hash";} ) if constexpr( std::is_convertible_v<CPP2_TYPEOF("hash"),std::string> ) return "hash"; else return std::string{}; else return std::string{}; }
[build] ^~~~~~~~~~~~~~~~~~~~~~
[build] /Users/filipsajdak/dev/execspec/external/cppfront/include/cpp2util.h:517:6: note: candidate template ignored: invalid explicitly-specified argument for template parameter 'C'
The issue is caused that enums cannot match to any provided is
function overloads. To handle that we need special overload for enums.
the following code segfaults:
using while first <= last {
instead of while first < last {
To Reproduce
1.
main: () -> int
= {
words: std::vector<std::string> = ( "decorated", "hello", "world" );
first: std::vector<std::string>::iterator = words.begin();
last : std::vector<std::string>::iterator = words.end();
while first <= last {
print_and_decorate(first*);
first++;
}
}
print_and_decorate: (thing:_) =
std::cout << ">> " << thing << "\n";
$ cppfront pure2-bounds-safety-iterator.cpp2 -p -s
pure2-bounds-safety-iterator.cpp2... ok (all Cpp2, passes safety checks)
$ g++ -std=c++20 pure2-bounds-safety-iterator.cpp -o pure2-bounds-safety-iterator
$ ./pure2-bounds-safety-iterator
>> decorated
>> hello
>> world
Segmentation fault (core dumped)
I wanted cppfront to reject it
it it passed and segfaulted on running
I really liked the fprintf
demo in the cppcon talk, but either I'm missing something (more likely) or it simply cannot work (sad).
In cpp1+cpp2 mode, let's assume we have this code:
struct S
{
int a;
};
get: (s: S) -> int = s.a;
Now we can call both:
a := get(s)
b := s.get()
So far so good. But let's say that S
is actually defined in some library's header file, and the author decides it would be a good idea to add a member callable, be it a member function, lambda, std::function
, function pointer, or whatever, that is also called get
.
In cpp1, that would be totally fine. In cpp2 however, it seems to me that it would break everything. Even if you add a rule saying that a member callable takes precedence over the one we defined in the first bit of code, that would still mean that b
's assignment now calls a different function, without us changing any of our code, only because of a library update that would be a-OK in cpp1. Backwards-compatibility broken, silently, and unexpected behavior that will be very fun to debug, I'm sure.
But it gets worse: if we define get
as a template instead:
get<T>: (t: T) -> int = ...;
part of the design goals as far as I understand is making cpp2 more toolable. But without concepts or something, Intellisense would probably show this as a member function of every single variable in your code.
Oh, and what about more basic stuff, like factorial? Cause this syntax looks kinda weird imo:
fact: (n: int) -> int = n ? (n * fact(n - 1)) : 1;
main: () -> int = {
std::cout << 5.fact(); //do I really want Intellisense to recommend every function that takes an integer whenever I type an integer?
}
I don't know, maybe I'm missing something, but it seems like something to think about for sure.
Hi Mr. Sutter. Really loved the talk (finished watching today) and appreciate your work on the language and C++ in general.
About 3 minutes ago I just got off of a Skype call with my brother. He's still new to C++, whereas I've been with the language for around 14-ish years. He was having some trouble with a segfault in a very simple project and it took me a while to figure out what the exact issue was. This is definitely something I've shot myself in the foot with quite a bit; or at least it created all sorts of headaches when it came to debugging.
In short, here's what we had up:
ComplexWidget.hpp:
class CompexWidget : public QWidget {
public:
ComplexWidget();
private:
QLineEdit *m_numberValue;
};
ComplexWidget.cpp:
ComplexWidget::ComplexWidget() : ...
{
QLineEdit *m_numberValue = new QLineEdit("");
}
...
void ComplexWidget::onButtonClicked()
{
int n = m_numberValue->text().toInt();
}
Whenever he pressed his button, hoping to get the value from m_numberValue
and parse it to an integer, we were treated with a segmentation fault. It wasn't until some debugging did I spot what the true problem was. In the constructor, he re-declared m_numberValue
. I told him to get rid of the QLineEdit *
in the constructor and then his program worked, with no segmentation faults. This actually took me a little bit to hunt down as well. Not once, when we were compiling, was there any warning about this. This is an issue related to scoping.
As for what to do to prevent this, making things safer, my proposal is: Don't allow programmers to re-declare variable names. Give a compilation error instead. I'm sure some other rules could be added to this.
I was experimenting a little bit and discovered that UFCS would not work on a pointer that is being de-referenced for example:
main : () -> int = {
life := 42;
p := life&;
life.someFunction();
p*.someFunction(); // Not UFCSed
}
someFunction : (x : int) -> int = {
// Do something useful...
}
Will emit the following Syntax1 code:
// ----- Cpp2 support -----
#define CPP2_USE_SOURCE_LOCATION Yes
#include "cpp2util.h"
#line 1 "helloworld.cpp2"
[[nodiscard]] auto main() -> int;
#line 9 "helloworld.cpp2"
[[nodiscard]] auto someFunction(cpp2::in<int> x) -> int;
//=== Cpp2 definitions ==========================================================
#line 1 "helloworld.cpp2"
[[nodiscard]] auto main() -> int{
auto life { 42 };
auto p { &life };
CPP2_UFCS_0(someFunction, life);
*p .someFunction(); // Fails to compile!!
}
[[nodiscard]] auto someFunction(cpp2::in<int> x) -> int{
// Do something useful...
}
Which fails to compile since we are trying to call someFunction as a member of p
instead of running it through the UFCS system. I'm not entirely sure if this is intended behavior or not... but either way it is a little unintuitive.
UFCS is not triggered when a function is called as a class method on a class member variable.
main: () -> int = {
p := std::pair(1,2);
p.first.ufcs(); // bad! compiles to p.first.ufcs() instead of CPP2_UFCS_0(ufcs, p.first)
}
The current implementation handle only member function calls syntax on one variable. Below are some examples of what works and what is not working + what changes with #18.
main: () -> int = {
i := 42;
i.ufcs(); // works
j := fun();
j.i.ufcs(); // doesn't work
fun().i.ufcs(); // doesn't work
k := fun().i;
k.ufcs(); // works
get_i(j).ufcs(); // works with https://github.com/hsutter/cppfront/pull/18
get_i(fun()).ufcs(); // works with https://github.com/hsutter/cppfront/pull/18
res := int(42).ufcs(); // works with https://github.com/hsutter/cppfront/pull/18
int(j.i).ufcs(); // works with https://github.com/hsutter/cppfront/pull/18
}
ufcs: (i:int) -> auto = {
return i+2;
}
fun: () -> (i:int) = {
i = 42;
return;
}
get_i: (r:_) -> auto = {
return r.i;
}
I know that UFCS is still a work in progress - I am reporting that to track that. I fall into that issue when working on my small project. I was returning variables using multiple return values syntax and I was trying to trigger UFCS on one of the member variables.
Cppfront recognized a line as a cpp2 line but it is in the middle of a raw string literal.
Cpp2 code:
auto a = R"(
i:int = 42;
)";
after compiling with cppfront we get:
// ----- Cpp2 support -----
#include "cpp2util.h"
auto a = R"(
)";
//=== Cpp2 definitions ==========================================================
int i { 42 };
https://godbolt.org/z/qMnerz8cM
Expected: cppfront should treat raw string literals similarly as it treats /* */
comments - there is a need to check if it is in the middle of a raw string literal (like we test if it is in the middle of the comment).
While implementing semantic analysis I noticed a conceptual problem with the order-independence approach of cpp2. Consider this code here:
foo: (x : int = bar()) -> int = { return 3*x+1; }
bar: (x : int = 2) -> int = { return x/2; }
main: () -> int = { return foo(); }
cppfront happily accept that code, but it then fails in C++ because the order of forward declarations is wrong. Swapping the order of foo and bar fixes that problem. (Modulo another bug that the default argument is repeated at the declaration site, which also makes the C++ compiler unhappy).
One question is, what should the intended semantic be? Can default arguments reference functions that are further down in the translation unit? Intuitive, if we fully bet on order-independence, the answer should be yes. On the other hand implementing that is actually quite subtle. Note that it is not always possible find a topological order, for example here:
foo: (x : int = bar()) -> int = { return 3*x+1; }
bar: (x : int = foo()) -> int = { return x/2; }
main: () -> int = { return foo(); }
Clearly that must trigger an error. We could detect this cyclic dependency, but it increases the complexity of the compiler. Another question is, is the compiler required to analyze default arguments if they are never evaluated? If not, we could simply translate them lazily on demand and check for cycles during expansion. (Though lowering that to C++ will be nightmare, this approach will only work of default arguments are expanded by cppfront itself and not by the underlying C++1 compiler).
Note that we will have exactly the same problem with auto
return types. These also require analyzing the function itself, which might depend on other functions further down.
I see basically two options. Either we forbid this, requiring that functions are defined before they are used as default arguments or with auto return types. Or we make function analysis lazy, which requires the compiler to topological order functions according to their dependencies, producing an error if that is not possible.
Hi, thanks for sharing this exciting experiment! So far, this rekindles my joy of writing c++ :D
I'm facing some issues with gcc11 and some regression tests:
For sanity, hello world works:
hlindstrom ~/devel/cppfront (main)$ cppfront regression-tests/pure2-hello.cpp2
regression-tests/pure2-hello.cpp2... ok (all Cpp2, passes safety checks)
hlindstrom ~/devel/cppfront (main)$ g++-11 -I/$HOME/devel/cppfront/include regression-tests/pure2-hello.cpp -std=c++20
hlindstrom ~/devel/cppfront (main)$ ./a.out
Hello [world]
However, pure2-type-saftey emits:
hlindstrom ~/devel/cppfront (main)$ g++-11 -I/$HOME/devel/cppfront/include regression-tests/pure2-type-safety-1.cpp -std=c++20
pure2-type-safety-1.cpp2: In function ‘void print(cpp2::in<std::__cxx11::basic_string<char> >, cpp2::in<bool>)’:
pure2-type-safety-1.cpp2:34:23: error: ‘setw’ is not a member of ‘std’
In file included from regression-tests/pure2-type-safety-1.cpp:2:
//home/hlindstrom/devel/cppfront/include/cpp2util.h: In instantiation of ‘void cpp2::deferred_init<T>::construct(auto:45&& ...) [with auto:45 = {const char (&)[5]}; T = const char*]’:
pure2-type-safety-1.cpp2:32:28: required from here
//home/hlindstrom/devel/cppfront/include/cpp2util.h:392:82: warning: placement new constructing an object of type ‘const char*’ and size ‘8’ in a region of type ‘std::aligned_storage<8, 8>’ and size ‘1’ [-Wplacement-new=]
392 | auto construct (auto&& ...args) -> void { Default.expects(!init); new (&data) T(std::forward<decltype(args)>(args)...); init = true; }
| ^~~~
//home/hlindstrom/devel/cppfront/include/cpp2util.h:380:49: note: ‘cpp2::deferred_init<const char*>::data’ declared here
380 | std::aligned_storage<sizeof(T), alignof(T)> data;
| ^~~~
//home/hlindstrom/devel/cppfront/include/cpp2util.h: In instantiation of ‘void cpp2::deferred_init<T>::construct(auto:45&& ...) [with auto:45 = {const char (&)[6]}; T = const char*]’:
pure2-type-safety-1.cpp2:33:26: required from here
//home/hlindstrom/devel/cppfront/include/cpp2util.h:392:82: warning: placement new constructing an object of type ‘const char*’ and size ‘8’ in a region of type ‘std::aligned_storage<8, 8>’ and size ‘1’ [-Wplacement-new=]
392 | auto construct (auto&& ...args) -> void { Default.expects(!init); new (&data) T(std::forward<decltype(args)>(args)...); init = true; }
| ^~~~
//home/hlindstrom/devel/cppfront/include/cpp2util.h:380:49: note: ‘cpp2::deferred_init<const char*>::data’ declared here
380 | std::aligned_storage<sizeof(T), alignof(T)> data;
| ^~~~
Adding
#include <iomanip>
to the generated cpp file fixes the error but keeps the warnings.
Running the test segfaults:
Reading symbols from a.out...
(gdb) r
Starting program: /home/hlindstrom/devel/cppfront/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
d is int? false
*** stack smashing detected ***: terminated
Program received signal SIGABRT, Aborted.
__pthread_kill_implementation (no_tid=0, signo=6, threadid=140737348137920) at ./nptl/pthread_kill.c:44
44 ./nptl/pthread_kill.c: No such file or directory.
(gdb) threads
Undefined command: "threads". Try "help".
(gdb) bt
#0 __pthread_kill_implementation (no_tid=0, signo=6, threadid=140737348137920) at ./nptl/pthread_kill.c:44
#1 __pthread_kill_internal (signo=6, threadid=140737348137920) at ./nptl/pthread_kill.c:78
#2 __GI___pthread_kill (threadid=140737348137920, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3 0x00007ffff7b71476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4 0x00007ffff7b577f3 in __GI_abort () at ./stdlib/abort.c:79
#5 0x00007ffff7bb86f6 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7ffff7d0a943 "*** %s ***: terminated\n") at ../sysdeps/posix/libc_fatal.c:155
#6 0x00007ffff7c6576a in __GI___fortify_fail (msg=msg@entry=0x7ffff7d0a92b "stack smashing detected") at ./debug/fortify_fail.c:26
#7 0x00007ffff7c65736 in __stack_chk_fail () at ./debug/stack_chk_fail.c:24
#8 0x0000555555557840 in print (msg=..., b=false) at /home/hlindstrom/devel/cppfront/pure2-type-safety-1.cpp2:35
#9 0x0000555555558189 in test_generic<double> (x=@0x7fffffffd708: 3.1400000000000001) at /home/hlindstrom/devel/cppfront/pure2-type-safety-1.cpp2:27
#10 0x0000555555557627 in main () at /home/hlindstrom/devel/cppfront/pure2-type-safety-1.cpp2:8
Describe the bug
In cpp2util.h file there are duplicated symbols of functions:
cpp2::to_string
cpp2::report_and_terminate
cpp2::fopen
To Reproduce
Steps to reproduce the behavior:
main.cpp2
file:#include <iostream>
main: () -> int = {
std::cout << "Running my new app\n" << std::endl;
}
test.cpp2
file:
#include <string>
message: () -> std::string = {
return "hello from the cpp2";
}
src % clang++ --version
Apple clang version 14.0.0 (clang-1400.0.29.102)
Target: arm64-apple-darwin21.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
src % cppfront main.cpp2 test.cpp2
main.cpp2... ok (mixed Cpp1/Cpp2, Cpp2 code passes safety checks)
test.cpp2... ok (mixed Cpp1/Cpp2, Cpp2 code passes safety checks)
src % clang++ -std=c++20 -Icppfront/include main.cpp test.cpp
duplicate symbol 'cpp2::to_string(...)' in:
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/main-b0e56a.o
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/test-c3d2ea.o
duplicate symbol 'cpp2::report_and_terminate(std::__1::basic_string_view<char, std::__1::char_traits<char> >, char const*)' in:
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/main-b0e56a.o
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/test-c3d2ea.o
duplicate symbol 'cpp2::fopen(char const*, char const*)' in:
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/main-b0e56a.o
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/test-c3d2ea.o
duplicate symbol 'cpp2::to_string(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)' in:
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/main-b0e56a.o
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/test-c3d2ea.o
ld: 4 duplicate symbols for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
duplicate symbol 'cpp2::to_string(...)' in:
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/main-b0e56a.o
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/test-c3d2ea.o
duplicate symbol 'cpp2::report_and_terminate(std::__1::basic_string_view<char, std::__1::char_traits<char> >, char const*)' in:
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/main-b0e56a.o
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/test-c3d2ea.o
duplicate symbol 'cpp2::fopen(char const*, char const*)' in:
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/main-b0e56a.o
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/test-c3d2ea.o
duplicate symbol 'cpp2::to_string(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)' in:
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/main-b0e56a.o
/var/folders/52/hk4pdfpj0qx83161z_d05ckm0000gp/T/test-c3d2ea.o
ld: 4 duplicate symbols for architecture arm64
Additional context
Add any other context about the problem here.
I need to check if there are no other duplicated symbols excluded due to preprocessor rules.
It is very trivial task which I met every day more than 5 years. And in C++ we should write something like
std::vector<int> v;
...
for (auto x : v) {
std::cout << x << ' ';
}
If we can replace this with
std::cout << v;
it also can reduce language complexity. This is very easy feature which exists in every modern programming language. But C++ does not have that : (
I am sure the code in
Lines 539 to 543 in fd0c99d
should cast to a pointer rather than to a reference:
return dynamic_cast<C const*>(x) != nullptr;
I was curious to see if cpp2 improvements included iterator invalidation safety so I tried the following:
#include <vector>
main: () -> int = {
v : std::vector = (1, 2, 3, 4, 5);
i : int = 0;
for (auto& item : v) {
if i > 3 {
v.resize(100);
}
i += 1;
}
}
I got the for loop syntax from other regression tests but cppfront doesn't accept it. It appears to be complaining about imbalanced parens but AFAICT they are balanced. Errors:
~/cppfront/regression-tests$ ../cppfront resize-invalidation-check.cpp2 && g++-10 -fconcepts -std=c++2a -I../include resize-invalidation-check.cpp
resize-invalidation-check.cpp2...
resize-invalidation-check.cpp2(7,21): error: unexpected text - expression-list is not terminated by ) (at ':')
resize-invalidation-check.cpp2(7,23): error: expected valid range expression after 'for' (at 'v')
resize-invalidation-check.cpp2(7,24): error: expected ; at end of statement (at ')')
resize-invalidation-check.cpp2(7,24): error: invalid statement in compound-statement (at ')')
resize-invalidation-check.cpp2(7,24): error: ill-formed initializer (at ')')
resize-invalidation-check.cpp2(3,1): error: unexpected text at end of Cpp2 code section (at 'main')
resize-invalidation-check.cpp2(2,0): error: parse failed for section starting here
resize-invalidation-check.cpp2: In function ‘int main()’:
resize-invalidation-check.cpp2:12:15: error: request for member ‘resize’ in ‘v’, which is of non-class type ‘int’
12 | }
| ^
Also not sure what prevents call to resize... in generated code v
is shadowed, not sure if this is incidental or some deliberate attempt to prevent what I'm doing?
The initialization safety guarantee informs that the global variable is a local variable and that it needs to be initialized before the local variable in the function.
i : int;
main: () -> int = {
j : int;
j = 42;
i = 12;
}
Compile it with cppfront and we get an error:
cppfront % build/external/cppfront external/tests/global_variable.cpp2
external/tests/global_variable.cpp2...
global_variable.cpp2(5,5): error: local variable i must be initialized before j (local variables must be initialized in the order they are declared)
==> program violates initialization safety guarantee - see previous errors
Expected behavior: i
is a global variable and (according to the error message) should not be considered in checking the order of initialization of the local variables.
Using capture and if failed if done in some order.
In this cpp2 code cppfront fail on if i$ + n < std::ssize(line$)
:
lex_process_line: (line: std::string) = {
i := 0;
peek := :(n : int) -> char = {
if i$ + n < std::ssize(line$) {} // it triggers assertion
return '\0';
};
}
https://godbolt.org/z/bTvzW4P6z
On Compiler Explorer it fails with the error:
cppfront: source/cppfront.cpp:1561: void cpp2::cppfront::emit(cpp2::postfix_expression_node&, bool): Assertion `n.expr' failed.
Compiler returned: 139
On my setup, it fails with the error:
Assertion failed: (cap.capture_expr->cap_grp == &captures), function build_capture_lambda_intro_for, file cppfront.cpp, line 1495.
The issue can be solved by changing the order of variables in the if statement:
lex_process_line: (line: std::string) = {
i := 0;
peek := :(n : int) -> char = {
if n + i$ < std::ssize(line$) {} // works
return '\0';
};
}
https://godbolt.org/z/h8PGEs39K
by using parentheses:
lex_process_line: (line: std::string) = {
i := 0;
peek := :(n : int) -> char = {
if (i$ + n) < std::ssize(line$) {} // works
return '\0';
};
}
https://godbolt.org/z/4v7YxevY7
Replacing n
with the constant also works:
lex_process_line: (line: std::string) = {
i := 0;
peek := :(n : int) -> char = {
if i$ + 2 < std::ssize(line$) {} // works
return '\0';
};
}
https://godbolt.org/z/hcefPbers
My expectation is that cppfront does not crash. It should work with various orders of variables or should show the error message if there is some preferred order.
cppfront adds a call to value()
method when trying to get one of the values returned from a function that returns multiple values (happens when function is called in other cpp2 function).
cppfront compiles below code
vals: () -> (i : int) = {
i = 42;
return;
}
main: () -> int = {
v := vals();
v.i;
}
int cpp1fun() {
v = vals();
v.i;
}
into
// ----- Cpp2 support -----
#include "cpp2util.h"
struct vals__ret {
int i;
};
[[nodiscard]] auto vals() -> vals__ret;
[[nodiscard]] auto main() -> int;
int cpp1fun() {
v = vals();
v.i;
}
//=== Cpp2 definitions ==========================================================
[[nodiscard]] auto vals() -> vals__ret{
cpp2::deferred_init<int> i;
i.construct(42);
return { std::move(i.value()) };
}
[[nodiscard]] auto main() -> int{
auto v { vals() };
v.i.value();
}
In main()
function v.i.value()
cannot compile as int
has no value()
method.
The C stdio UFCS example got me thinking about a defer
control flow construct which would run the statement or block after it when the current scope would be exited. This would allow the the C stdio example to be rewritten as:
main: () -> int = {
s: std::string = "Fred";
myfile := fopen("xyzzy", "w");
defer myfile.fclose(); // Run at the end of the scope
myfile.fprintf( "Hello %s with UFCS!", s.c_str() );
}
Which doesn't do much in this example, but in more complicated cases where error handling gets involved it could become useful. I know RAII wrappers are the "idiomatic" C++ solution to this problem, but there are times (especially while prototyping) where writing a wrapper is unnecessarily clunky. In the past I have used some macro and lambda wizardry along with some scope guard libraries to implement almost this exact functionality in Syntax1. Thus implementing backend support for this type of feature in the cpp2 supplemental library should be straightforward...
But with all of this factors considered, would this sort of feature even be worth including in Syntax2? Or is it just another unnecessary feature to steal from Go for the sake of stealing it?
The D0708/A.4 Section "Multiple in
and/or forward
in the same definite last use" section has wording for avoiding an exponential number of function calls, but I don't think it does that.
In cpp2:
void func(auto&& w, auto&& x, auto&& y, auto&& z);
f: (in w : std::string, in x : std::string, in z : std::string, in w : std::string) = {
func(w, x, y, z);
}
I would expect a 16-way switch here, as each of the four invented parameters of func
will be deduced as either std::string
or const std::string&
. What is the utility of the four bullet-point subsumption language? You have to perform OR for each combination of argument value categories, and you're necessarily locked into the bad brute-force implementation.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.