The Clang AST is a rich, fully type-resolved representation of C++ code that uses an optimized design with ASTContext for centralized storage, where nodes point to shared resources rather than storing full copies, and provides tools like AST dump and AST Matchers for navigation and analysis.
Clang AST Tutorial: C++ Tool Development Insights.
Added:MANUEL KLIMEK: Hi.
So my name's Manuel Klimek.
I work at Google.
And I want to give a tutorial on Clang AST today.
Why am I doing that?
Something I care a lot about is that I want C++ to actually arrive in the 21st century.
And part of that is that you need really good tools.
A lot of other languages are way ahead in terms of tooling.
And now we have Clang, and we have a lot of great infrastructure to build tools on, but the problem is to build those tools you need a working knowledge of the Clang AST.
So I really want to write those tools.
I cannot write all of those tools myself.
So I need you to help me write those tools.
So I will just put the Clang AST in your face and hope that afterwards you have enough knowledge of the Clang AST that you will think, oh, I should write a tool.
So when I talk about working knowledge about the Clang AST, what I'm talking about is you need to know the basic structure, the kind of classes that are there, how you can navigate them.
And tools that help you learn more about the Clang AST.
I've been working with the Clang AST for probably two and a half years now.
And I still learn something new every day.
So the most important part is to learn how to actually figure out what's happening.
Short raise of hands, how many of you do not think they have a working knowledge of the Clang AST?
OK, that's enough people so that talk actually makes sense.
Hm?
AUDIENCE: Is this a front end?
MANUEL KLIMEK: Yes, exactly.
So let's talk about the very basic things first.
What makes Clang AST special?
It's a very rich AST representation compared to what you have in other languages.
You have a very accurate representation of the source locations for every node in the AST.
It is also fully type resolved.
It just kind of comes from C++.
You cannot parse C++ without doing the type resolution, so you don't have a fast way to get a non-type result AST.
And it's also huge.
It's more than 100,000 lines of C++ code.
And that's just the AST.
That is not like the semantic analysis or anything.
It's just the class and the AST.
So let's look at those classes in the AST.
And the first thing you'll notice when you look at Clang AST is that it's highly optimized for size and speed.
And especially size.
In C++, all of us have those huge transitive inclusions and you have ASTs for all of them.
So your nodes need to actually be smaller.
So the first class you always encounter when handling the Clang AST is what's called the ASTContext.
A lot of the nodes in the AST just keep small identifiers that point into stuff in the AST context.
Like identifiers are not stored with every node, they're stored in an identifier table.
And the nodes just point in there.
And the same is for locations.
We will see the location stuff in much more detail later.
And the other thing, the AST context provides us the entry point into the AST.
Which is a function called getTranslationUnitDecl().
That gives you the translation unit Decl, and with that, we basically go into the core classes of Clang AST.
Those are declaration statements and types.
Those are the classes that have identity in the Clang AST, and they also have point identity.
Which means if you have two of them, you can compare their point as to figure out whether they're the same.
So let's look at some examples of what those classes might be.
For each of those classes is the base of a whole hierarchy of classes.
There are lots of different kinds of declarations, and they are all inherited from Decl.
There are lots of different kinds of statements.
They're all inherited from statement.
The interesting part is, as compared to other ASTs, there is no common base class.
Which also means there is no common way to visit all of those.
But we will go into that in a moment.
So first, let's take a look at some examples.
CXXRecordDecl.
Which is one of the core things if you handle C++ that you will encounter.
Now why is it called CXXRecordDecl and not Class Decl or something?
And that's where I start teaching you tutorial style.
The first thing you need to learn is whenever you encounter a class in Clang AST, we have this great Doxygen documentation.
You just go to your favorite search engine, and you enter Clang and CXXRecordDecl, and there, the first link usually is exactly the documentation for that.
And then you can find all the members.
And at some point you will also find sometimes a short explanation of what this is.
So yes, this represents structs, unions and classes.
That's why it's called a RecordDecl.
But there are also other declarations, variable declarations, and one reason why Clang's AST is so big because C++ is so complicated.
You have stuff like an unresolved using type name declaration.
Great.
Then statements.
Let's look at some common type of statements.
The compound statement, the most basic statement you can get.
You have things like a CXXTryStint, and you have a BinaryOperator.
BinaryOperator is an expression.
In Clang's AST, expressions are statements.
And if you ask Doug Gregor on IFC, you will kind of, even though it's purely text based, you will basically seem him go, [NOISE] Yeah, I was young and I thought that was a good idea to save some bytes in the AST, and he would also like, if you could fix that, if anybody of you has the time to put in to fix that, that would be great.
One of the interesting parts that we will see later is that this makes it really hard.
We don't have a place to store the semicolon, like where we reference the semicolon from.
And if you do source to source transformations, like some people do, that's a problem.
And then we have types.
Types in C++ are also complex.
There are many different types.
Types can be nested, like a PointerType.
There are ParentTypes, and there are strange things like SubstTemplateTypeParentType.
Again, Clang AST is huge because of C++.
We have those basic three classes that have identity, but when you work with a Clang AST you will find a lot of different other classes.
And those classes are what I call Glue Classes.
They are basically there to get you from one place in the AST to another one.
For example, DeclContext is something that some of the declarations inherit from, if they can contain other declarations.
TemplateArguments can be different types of notes.
NestedNameSpecifiers model certain kinds of a declaration or something.
And QualTypes we will see in more detail in a moment when we look into types.
And then-- now we have all those classes, but as I said before, there's no common interface to all those classes.
So how do you actually traverse the AST?
And that's what I call Glue Methods.
Or you can call them Traversal Methods or something.
So every node, every special node of a certain type has methods that give the nodes it connects, right?
It's easiest if you look at an example.
If you look at an if statement, an if statement clearly has a then statement, an else statement, and you traverse that by actually calling that method on the if statement.
But Clang's AST is not only a tree.
It's actually a graph, because you can jump from parts of the AST to other parts.
As with AsCXXRecordDecl(), you can get the described class template.
We'll go some more into templates later, but not into too much detail because that would be a whole talk on his own.
And interesting things like if you have a type and that type is a CXXRecord type or class type, then you can get to the declaration of that class from the type.
So now we get to types.
Types are complicated.
Why are types complicated?
In C++, you have very simple examples.
Like you have an int x and a const int x.
And basically the int is the same underlying type, and you have qualifiers on the type.
And if you want to store all of those different combinations of types, you basically have an explosion of combinations.
So you don't want to do that.
So how Clang models this, is it has a QualType, which contains the qualifier to the type, and that points to its type.
Sounds pretty simple.
When we get into more complex examples, that's not as simple anymore.
Because you can have nested types.
So if you have something like this, the qualifier can be in the middle of that type.
So if you look at a PointerType and how you traverse, like how you get to the type it points to, you get a QualType out.
So if you look at that for a simple example, like an int pointer.
The int pointer basically is a QualType which points to its type, which is a pointer type.
And then you can traverse from that to the built in type by going over the QualType.
So enough of types, let's talk about location.
Locations are also very interesting.
Locations are really, really clever.
They are kind of genius, but mad genius.
So at first glance, locations are pretty simple.
Locations are just an ID and the real information behind those locations are stored in the source manager, because there are so many locations all over the AST.
You don't always want to store the full line column stuff.
They are very cleverly encoded in this ID, so you have common operations very fast.
And you always point to tokens because you have a Lexer, so if you point to tokens, you can always get the information about the length of a token later.
So let's look at how you get to locations for the common core classes we have.
So let's start with the declaration.
Declarations are actually pretty simple and have some very common ways on how we deal with them.
The first thing you'll notice is that they usually have a getLocStart() and getLocEnd() which points to the first token and the last token respectively.
In this case, the closing brace is the last token.
If that was longer it would actually point to the first to the start of that token.
Then declarations have a method called getPosition() that give you the position of the identifier of the thing that is declared here.
And because this is a method declaration, you can get a qualifier location, which will give you the first-- and this should give you a range, actually, which is from the first to the last token of that qualifier.
Now let's look at statements.
Statements are much more interesting because they can look many, many different ways.
First, they have a start and end location too, but now for everything else we have a very simple method call here.
And for everything we want to get at, we actually need to drill through what is actually there.
We have a method call here, and that has a callee.
And that callee has you could get the base expression.
Which can be any expression.
In this case, I have a variable, so it's a declaration reference expression.
And the declaration reference expression has a NameInfo in it, which I can get out.
And from that I can get the location, and then I'm finally in the location of that.
And the location of that.
And to get the location of the right part here, again, the callee is a member expression in this case.
And because it's a member expression, it has a MemberNameInfo.
And I can get the location off that.
This is simplified, right?
If you want to do that on an AST, you actually have to put dynamic casts in here everywhere.
Because we're not sure that this is actually a member expression if you just see a function call.
The interesting part is if this here was a static call, it would be completely different to do all that.
Like if this is a static function, even though I address this via a variable, I have a completely different representation of that in the AST.
All right, let's look at my types again.
Types are usually complicated.
With locations they're actually not that complicated, because the interesting part about types is that the location is decoupled from the type.
Because I can reference a type in many places in my program.
Here, for example, I have a declaration MyClass is the type.
I have it as a variable declaration, as a perimeter declaration.
And Clang models the location of types as TypeLoc.
So I have two different TypeLocs that all point to the same type.
And again, when you have a TypeLoc you have the well-known getLocStart() and getLocEnd() that give you the first and the last token of the type in that case.
It doesn't point to qualifiers.
I actually have no idea how to get the creation of the qualifiers, but I have never needed that.
Now if you put all that together, what we just learned for types, we get an interesting picture.
This is just for this simple pointer declaration.
The interesting part is that the TypeLocs form a shadow hierarchy to the types.
And then we have for the types, we know already that there's always a QualType around them.
So the whole thing looks like that.
If you have a PointerTypeLoc you can get to the inner TypeLoc points by saying getPointeeLoc() But from the TypeLoc, you can also get to the type through the QualType.
And then you can get to its inner type again through the QualType.
So enough about types.
As some people have mentioned on this conference before, there's a lot of interest in doing source to source translations.
And it is actually possible.
One of the interesting things you have to be careful about is when you want to do source to source translation, what we support and what works very well for us is you just get the text for something.
And then you do a textual modification.
The interesting part is when you try to get the text for some part of C++, there are macros involved.
I won't go into the details of macros and source location handling there, because it would take another half hour.
And it's really, really crazy.
But Clang provides-- in the Lexer it provides some abstractions that are intended to do the right thing.
For example, there's the makeFileCharRange, and you give it a range of source code.
And it tries to figure out through macros and all that stuff what text you actually mean.
There are some bugs in that that we are currently working on fixing, but it already works for the 99% case, actually.
And then, as you've learned before, the source locations always point at tokens.
So if you want the relocation of something, you have to measure the token length.
Also you need the Lexer for that.
There are also other ways to get the text.
And everybody seems to run into the same problems there, because you can get the text right from the source manager without doing any of the cool stuff that makeFileCharRange does, and you will just run into problems.
Then one other interesting thing about the Clang AST is how templates are handled.
I will not go into that into too much detail, but there are a few interesting things that is different from other compilers.
The first thing is that the Full AST of the template definition is available.
So it's modeled as a CXXRecordDecl that has dependent types inserted everywhere.
Where the type is not currently known.
And the interesting part is that, especially if you do source to source transformations, you can already do some source to source transformations on that template definition without needing an instantiation for it.
Of course, all instantiations are also available.
So that's really important if you want to index code, and you want to find where things are called from.
It's easy to find all places, in all instantiations where some other function is called from.
One interesting pitfall to look out for and one of the reasons why I'm not convinced that rewriting the AST itself and then doing something with it is a good idea.
For example, AST nodes can be shared between template instantiations.
So a node can actually have multiple parents, and there are lots of corner cases around that.
In the beginning, we've seen there is no common interface to traverse the AST.
And by having a common base class, but you need to know all the methods you need to call.
And to help with that, because we don't want everybody to have to write out all those dynamic casts all the time, there's the RecursiveASTVisitor, which is basically a macro monster putting together all this knowledge about how you traverse each and every node in the AST.
The interesting thing about that is you trigger on certain types you care about.
So you can say, let me visit all statements.
And then it will call you back for statements.
It does not give you context information around the parents or children.
You get a specified order, but you have to do all of the contextual putting it together yourself.
And to help with that, there are AST Matchers, which again, I won't go into too much detail.
I will show you examples later.
But for now, the important part is that they make the contextual stuff easier.
So they allow you to trigger on expressions of context.
And then you get a call back whenever that context is met.
But now let's go into something that's much, much more important.
And that's the tools that help you understand the AST yourself when you run into problems.
With that, we go into the code.
I hope that it'll be all readable.
So first, Clang itself can do AST dump.
So I have a very simple C++ file.
It doesn't have any dependencies.
So I can just say, [? -exclam-astdump?]
-f-- oops, syntax-- I cannot type.
So I just leave that away.
And minus link warning the end.
This will give me a dump of the AST.
We also have ClangCheck, which has an integration with the tooling framework we've built.
Which helps you to dump the AST for files and projects that are larger.
Where here, if I had some includes that are not standard includes, I would need to write out all the include paths.
So let's look into an example, let's look into some TableGen file in LLVM.
What we see here, for example, what might look interesting in the AST, they're returning something with lots of concatenation of strings, and we actually have no idea what's going on there.
So for that we can call ClangCheck, and we just give it the path.
And first we don't actually know how exactly those things are called.
I just remembered AddValue or something.
So we can do AST list, which will give us the whole list.
And you can just grab for stuff that's in there.
You see there's tgparser::addvalue add value.
So let's dump that.
And yes, let's type that into less, yes, less.
That's the AST dump of that whole function.
And if you just look at the return statement, that's a little big.
So we will not get into the detail of this, but this is how you can drill down into what's actually happening in the AST, in some random piece of code.
You use ClangCheck.
You use ASTDump.
You use ASTDumpFilter.
For now, we will just look at a very simple example.
I've prepared the code here.
We have some code that takes a constant string reference.
And we have code that calls it.
And you might notice that there is a call to c_str in here.
It's a string it's called c_str on a method that takes a standard string reference.
Those things actually happen in reality.
Because this function might have originally been taking a const char pointer.
And somebody thought, hey, we have 2010, perhaps that's not the greatest idea.
Let's change that to take a constant string of reference.
Obviously, this code doesn't break, so nobody notices.
Nobody changes it.
We waste CPU.
So you might want to actually find those cases in the code.
And that's where Clang can help.
So we can look at how that looks.
Again, it's pretty huge.
The interesting part is that the lower part here is all the AST dump of the actual CPP file.
And now I have prepared some slides to actually guide you through how to read such an AST dump.
Because at first it just hits you over the head.
So let's look from the inside out.
is there?
First, we have the call to s.c_str() in the innermost call.
And that's a CXX member call expression.
And the.c_str is a member expression, and it's on the variable s, which is a declaration reference.
And then around that-- and that's why the AST's are so huge in C++-- there's all this implicit stuff going on.
In this case, there's an implicit construction of a string.
Because the function that we're calling actually takes a string reference.
So now there's the materialized temporary expression.
A bind temporary expression.
And in that is the construct expression.
One thing that's often not intuitive to people coming new to that AST, a construct expression is an implicit construct.
We will see how the explicit construct looks in a moment.
And that actually takes two parameters.
And the one parameter is the const pointer from the call to s.c_str, and the other one is the allocator.
And around that, we have the call to f, which is then the call expression.
And again you find that DeclRefExpr to the actual function being referenced.
And now one interesting thing to look at is what if you do a very simple change to this code?
And explicitly write the conversion to standard string in here.
And now let's look at how the AST changes.
We got two new nodes.
And we got this, which is the CXX function and the cast expression, which models the explicit writing of that standard string function cast.
So that's basically the way you figure out how when you read the AST dump, what happens.
Now we go into another example.
And that's basically a playground.
When you want to learn about the AST.
That's a unit test, actually.
That's pretty much the smallest unit test I can write for the AST.
It uses the AST Matchers.
The details are not really important.
This is some set-up.
Here we initialize an AST Matcher that matches RecordDecls, and we bind that to the IDX.
And then more set-up, and we actually run it over this code here, structx.
And then we get a call back.
And in the callback, the extract, the thing we bound, and then we dump it.
Clang's AST node often have a method called Dump or something similar.
Sometimes they need a source manager or something, but in general, that's how you can debug the AST.
So let's run that.
And now you see what matched.
The people in here, very familiar with the intricate details of C++, will immediately know what's going on.
When I saw that the first time, I was like, what?
Why does it match two times?
And then Chandler was so nice to explain to me that there's an injected class name, which is called x::x.
So first, we visit the class definition.
And that class definition has the injected class name.
And that's why we have that both here.
And then we again visit the injected class name because it also is a CXXRecordDecl.
So we don't want to see that, so we can change the code to just, for example, say has name::x We only want to see RecordDecls that have the name::x.
We recompile.
We rerun the test.
And now we only see the outer one.
Now we can play around some more with that.
Let's say we make that template X. And let's do the instantiation of that.
And now we see what you get when you have a template instantiation.
Again, first you get the CXXRecordDecl that's the definition.
Like the template definition.
And then you get a class template specialization decl which all the types resolved.
And some implicit stuff going on here.
And then we can, for example, I told you earlier you can get locations out of the stuff.
How do those locations look?
We have source, location, or we just dump them.
So we just we have our-- AUDIENCE: Can you turn that off?
Not now, in general.
MANUEL KLIMEK: What?
AUDIENCE: The pop-down.
MANUEL KLIMEK: Yes, I like the pop-down.
Hm?
AUDIENCE: Can it be turned off?
MANUEL KLIMEK: It has to be turned on.
AUDIENCE: It's Vim.
MANUEL KLIMEK: It's Vim, yes.
What can you not turn off in Vim?
But I kind of like semantic code completion.
I don't have memory, so-- that worked.
Now we, for example, dump the source location.
Unfortunately, I didn't enter a new line, so it's kind of mixed in.
But you can see now an example in line 123, that's somewhere here.
You get that hit to the declaration.
Yeah and with that, I'm at the end of my tutorial.
There are actually many links to documentation.
While documentation is still sparse, if you run into any problems, we are always very happy to get patches to the documentation.
And with that, we're open to questions.
Or if you want to see stuff that we can figure out or not how it works in the AST, you could also just throw out ideas of what we could do here.
AUDIENCE: I have questions about AST for invalid code.
For example, from what I've seen, if you are referencing a function that is in a header was not found for example, or something like that.
The whole expression is removed from the AST.
Is there any way to get to a more basic AST with nodes?
Where function's code are not solved but are not removed?
MANUEL KLIMEK: No, I don't think so.
But I'm also, I don't know, but perhaps, Chandler, Richard can answer that question.
Richard can.
AUDIENCE: The answer is no.
AUDIENCE: Wait, wait, wait.
Mic.
AUDIENCE: So the answer is no.
We don't support building ASTs which don't obey our AST invariates.
So whenever we build an expression, we want to make sure that it's actually valid.
Otherwise it's absolutely possible you're probably going to have a lot of trouble with it.
MANUEL KLIMEK: I think an interesting question would be what you need it for.
Because for code completion, Clang has its own mechanism, right?
It inserts basically a code completion token and then tries to fall back, like to do recovery.
At that point and then that's how it does code completion.
If you want to, for example, form an invalid code.
If you were in Danielle's talk, we actually have a parser for that, but it parses something that looks like C++.
So the question is, what do you want it for?
AUDIENCE: I"ve got a question regarding this recursive AST visitor.
So the problem I had with this visitor is there's this huge macro monster and when something explodes, it's almost impossible to debug.
Because you have all these macros, and you have no clue what's going on.
Have you some advice about debugging?
MANUEL KLIMEK: Well, there are two ways.
One is, we have built the AST Matchers for a reason.
And we are currently in the process, like a colleague of mine is contributing a way to dynamically construct those AST Matchers, so you can play around with them much more easily.
And I think that's a good way to actually see where you're trying to get.
If you really want to work with the recursive AST Visitor directly for some reason, put on a debugger and hope that somebody implements macros for the debugger.
No.
Do we have an answer?
Chandler was there when this macro monster was introduced, so I couldn't fight it.
CHANDLER: I'm sorry about the macro monstrosity.
I actually argued against it, when it was being implemented.
And I was shouted down from many points.
So my first question, of course, is are you trying to debug at compile time or at run time?
Most people have trouble debugging the AST at compile time, more than at run time.
And the compile time errors, I think the best technique is to use Clang to compile.
And it actually produces pretty readable error messages, even for the recursive AST visitor.
And if you don't think the error message you get from your compiler is reasonable, you should file a bug about that.
Because we actually like to fix those.
If your bug is at run time, I've actually never really had a problem stepping through the runtime behavior, the recursive AST visitor.
But I think Eric and others would also appreciate bugs there to have better debug info.
MANUEL KLIMEK: And you have printf You can change that.
What I actually did by writing the AST Matchers is just changing the recursive AST visitor to put in step by step output of what it's doing.
But it's complex, yes.
That's basically the price you pay for not having that common interface.
AUDIENCE: So is there a way when you want to search for certain pattern when you want to switch on the variable of a certain type?
That you could search for the variable and then find back to parent to see if it is a switch?
MANUEL KLIMEK: Yes.
Do you want me to type that out?
You could just write an email to the list, and we can answer that there in more detail.
And iterate on what you actually want to do.
But in principle, that's all there.
It's not like it landed like a few months back.
So it's pretty new that we have a parent map in the AST context that actually allows efficiently to do matches on the context.
And the AST Matchers have a method where, in the callback, up here, I can just call match on the node I have.
And I have a half parent, for example, there a half parent, switch statement.
But the problem with that is always in the details.
What implicit things can be in there?
That's the hard part.
AUDIENCE: I had a problem with the AST match recently.
I wanted to use to transform in the callback, but the callback uses an AST consumer, and it's not a SEMA consumer and the class is not mutable You cannot subclass it.
MANUEL KLIMEK: What did you try to do?
AUDIENCE: Well, to modify the nodes to do a source to source transformations.
MANUEL KLIMEK: Yes, so my advice is don't do that.
So we do source to source transformations by producing in the callback.
We produce replacements.
Let me see whether we find that.
So the idea is that you generate a list of replacements, and those you can construct when you're in the callback.
And we actually have also some infrastructure to apply them in the end.
Because one interesting part about that is that you have to deduplicate them.
Because when you visit headers, you may do the same, or actually, you may try to do multiple conflicting edits at the same time.
Let's say you do something on templates, so you might want to edit a template definition in different ways.
That doesn't work.
So we actually need to go through all the places where things are happening, collect all of the different edits you're trying to make, deduplicate them, or go back to the user and say, no you cannot do that, and then apply them.
And that is our recombinant way to do that.
And that works, we are doing that for two and a half years on 100 million lines of code.
So it definitely works.
It's not as much targeted to if you don't actually want to write out that source code.
If you just want to do-- and I don't really call that source to source transformation.
If more of what you want to do is you want to change the AST to just use the back end magic to produce code.
Which is a very different use case, I think.
Chandler has an opinion.
CHANDLER: This is not my own opinion.
I'm proxying Doug Gregor, because he's not here.
But some time ago, when we first contributed the AST Matcher infrastructure to Clang, one of the hopes was that we could actually use fine grained AST Matchers and the callbacks within them, to analyze the ASTs that we're in the process of building.
There's a lot of spaghetti code inside of the semantic analysis of Clang that we could potentially simplify by using library abstractions like AST Matchers.
However, it turns out that in all those cases we're actually pretty much OK not having immutable AST.
Because the region of the AST that we want to analyze is not the region we're trying to mutate.
And, in fact, it almost needs to not be the region we're trying to mutate, because otherwise we have a very challenging problem.
If we insert a new class into the AST, and that's a valid match for the Matcher we are running, do we match that or not?
Which way you answer that-- I mean either answer is possible, but which way you answer that changes the efficiency in the strategy of the implementation.
And it's much more clear that we are not going to match any new things you put into it, if the AST piece that you're walking over is, in fact, constant, is immutable.
So that was some of the original design ideas.
And what I suspect is that we still are missing some API changes necessary to enable that particular use case.
And I do think that's a valid use case, and we should make sure we-- MANUEL KLIMEK: --no we don't miss that.
So what I wanted to follow-up with, we actually allow you to shoot yourself in the foot there.
So you can now, not using the match finders like getting the front end action out of it, but just it has no methods.
So you can just run it on the AST node.
And you can do that anywhere where you have an AST node.
And that is also, you can put that in wherever you have AST nodes with SIMA available.
And you can do the matches, and then because you have this callback, you can bind something to your callback that lets you modify the AST.
So that works now.
MALE SPEAKER: Some more questions?
OK.
Thank you, Manuel.
Up Next

Static Analysis for Vulnerability Discovery | Bug Hunting
@ZCliffeSchreuders
16.1K views•2015-06-22

BitTorrent Protocol Explained: Piece Selection & Peer Choking
@StevenGordonAU
481 views•2013-02-22

HTTP Requests Explained: GET, POST, PUT, DELETE
@codecademy
103.1K views•2021-10-07

Enigma Machine Mechanics: WWII Encryption Explained
@JaredOwen
13.2M views•2021-12-11
Related Study Plans & Knowledge Roadmaps
Structured learning paths in Computer Science






































