-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: a re-implementation of the compiler backend #424
Conversation
An empty body does not mean that the procedure is imported. Check for the presence of the flag instead.
Stores references to the things that make up a module, e.g. procedures, globals, etc.
The `owner` field was only meant as a temporary measure and is going to be removed soon.
Some basic integer operations were missing.
The operators were swapped.
In addition, don't define referenced globals but declare them as `extern`.
The condition to whether overflow-checks should be inserted was inverted, causing `nimDivInt` to recurse infinitely.
'branch' currently means "jump if condition" not "jump if not condition"
Instead of requiring magics calls to be represented via normal procedure calls, they can now be represented directly with `ntkCall` nodes without the need for a `ProcId`. The old approach had a few problems. Since magics procedures can be generic, it's not possible to know all valid instantiations once reaching the backend. To still be able to insert magic calls, a pseudo-proc with no type information was created for each magic and cached in `PassEnv` This meant that `ProcHeader` together with all logic interacting with it had to support procedures with no type information. In addition, the cache for generated magics (`PassEnv`) had to be available wherever magics need to be inserted.
Don't use `PassEnv.magics` anymore. All compiler-inserted magic calls now have return-type information attached. Also fixes `bcOverflowCheck` having the wrong type when wrapped around `mInc|mDec`.
Reap the benefits.
Their initialization logic is still missing however.
It's possible for multiple `PType` instances of the same type instantiation to exist and the previous logic didn't account for it, causing type mismatch issues in the generated C code.
It's important that the unksipped types are passed to `requestType` so that imported types can be handled properly.
The previous logic looked at the type of the *argument*, but the correct way is to look at the type of the target *parameter*. `vmgen` has the same issue.
* correct some typos * improve some doc comments * remove dead code * leave some annotations about future directions
* correct some typos * improve some doc comments * remove dead code * remove stale annotations * leave some annotations about future directions
* correct some typos * improve some doc comments * remove dead code * remove stale annotations * leave some annotations about future directions
* correct some typos * improve some doc comments * remove dead code * remove stale annotations * leave some annotations about future directions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aside from a minor stylistic comment I made, everything else seems to be in order. Sadly, I can hardly promise to provide an in-depth review ATM (IRL stuff), but going from the provided readme I don't think I would have any high-level comments anyway.
for sym in g.compilerprocs.items: | ||
case sym.kind | ||
of routineKinds: | ||
p.compilerprocs[sym.name.s] = procs.requestProc(sym) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should introduce the func symStr(s: PSym): string = s.name.s
and use it, instead of continuing to write hundreds of naked field accesses in the code.
registerPass(graph, cgenPass) | ||
|
||
if {optRun, optForceFullMake} * conf.globalOptions == {optRun} or isDefined(conf, "nimBetterRun"): | ||
if not changeDetectedViaJsonBuildInstructions(conf, conf.jsonBuildInstructionsFile): | ||
# nothing changed | ||
graph.config.notes = graph.config.mainPackageNotes | ||
return | ||
else: | ||
graph.config.exc = excGoto # only goto exceptions are support for the new cbackend |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking out loud, a smaller PR we could pull out of this one is removing the other exception types.
#### IR overview: | ||
* the IR is a linear node-based representation | ||
* nodes reference each other via indices. A node can only reference nodes coming before it; reference cycles are forbidden | ||
* it's still undecided if a node may be referenced multiple times |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Within the AST proper, no.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do see this as useful for something like the empty node, but I'm not convinced if this is the right thing.
Use the new `rvmXXX` enum value names instead of the `rsemVmXXX` ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've done a first pass. I can only absorb so much in one go and around irpasses
I was really fading.
In terms of how to proceed as a strategy:
- we need to shrink dialect problems in devel
- unit tests directly against cbackend2
generateCode
My reasoning for the first point:
- even with the code working for say
refc
it's that much more to review and test - CI will be faster, I suspect test cycles are going to be a limiting factor here
- it'll simplify sem's output, which will clarify/simplify things further here, and likely cycle again reinforcing each other
- we need to do it anyways and it'll bring the goal of getting things merged a lot closer
The reasoning for the second point:
- Cyo will be a new language at this point (grammar and semantics), its frontend will be separate and I'd like to reuse the backend and VM
- tests will enforce and maintain strong separation
- they'll allow for refactoring without having to go through a frontend
Practically, I think it should work by us looking at and discussing CI failures and then:
- if you could makes fixes for the things we think should continue to work
- I can drop the dialects/whatever... for the things we're going to remove
Simultaneously, for issues you're encountering during implementing fixes:
- if you can add some direct unit tests to hold necessary properties in place
- I can try to make sem less dumb to make your life easier 🤞🏽
As those lines converge, we end up with all tests passing and an easy merge. Thoughts?
#### IR overview: | ||
* the IR is a linear node-based representation | ||
* nodes reference each other via indices. A node can only reference nodes coming before it; reference cycles are forbidden | ||
* it's still undecided if a node may be referenced multiple times |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do see this as useful for something like the empty node, but I'm not convinced if this is the right thing.
assert result.len == int(getSize(conf, s.typ)) | ||
# XXX: requiring the length to fit might help in catching some issues, but | ||
# it's too restrictive | ||
assert result.len >= int(getSize(conf, s.typ)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it instead introduce a new proc that is relaxed while keeping it strict elsewhere?
@@ -51,9 +51,9 @@ type | |||
CoDistinct | |||
CoHashTypeInsideNode | |||
|
|||
proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]) | |||
func hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Read this file, then thought:
🎶 won't you take me to... Func-y town🎶 😁
* nodes reference each other via indices. A node can only reference nodes coming before it; reference cycles are forbidden | ||
* it's still undecided if a node may be referenced multiple times | ||
* control-flow is represented via gotos and joins. Instead of storing the index of the corresponding `join` target, a `goto` stores an index (`JoinPoint`) into a list storing the actual IR indices. This is aimed at making IR modification simpler, by removing the need to patch goto targets in the IR directly. | ||
* there exist a few special experimental gotos (goto-link-back, goto-with-continuation, goto-active-continuation) meant for more efficient `finally` handling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Philosophical remark:
This feels like a theme in my experience.
It starts with a very small set of core primitives that are beautiful and all, great for learning and conveying key concepts. Then for making them actually useful a large set of variants are then introduced to encode constraints information.
|
||
Currently also ignores whether or not a hook is trivial and thus replaces the assignment for types that don't actually need/use a `=copy` hook. | ||
|
||
#### `refcPass` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly another PR, we drop refc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As refc
support is fully implemented already, I'd be a bit reluctant to remove it either before or as part of this PR and instead would rather remove it after. The implementation is then preserved in the git history at least.
One thing to consider is that the refc
, markAndSweep
, boehm
, and go
GC support share almost all of their implementation in the compiler, so only removing refc
would not reduce (by a significant amount) the required code/complexity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, ok we don't have to do it just yet.
As for the other GCs I would drop all of those as well.
# tables | ||
let fakeClosure = genFakeClosureType(env.types, passEnv) | ||
|
||
# XXX: mutable because they need to be swapped in and out of the ``RefcPassCtx``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More push for the no more refc case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue is not directly with refc
, but instead with first-class view-types being unfinished.
Both sequences need to be available to various passes (those using RefcPassCtx
), but passing them as parameters is not possible due to the TypedPass
interface, and storing them as part of RefcPassCtx
directly would be wrong, as they are not something that directly belongs to the context object.
The context object should ideally borrow from the sequences instead (via lent seq
in this case), but since that's currently not possible, I've used the swap-in-swap-out idiom as a way to mimic borrowing. Using shallowCopy
/.cursor
or a pointer don't require the source sequences to be mutable, but they have other downsides.
var ttc = TypeTransformCtx(graph: passEnv, ic: g.cache) | ||
var upc = initUntypedCtx(passEnv, addr env) # XXX: not mutated - should be ``let`` | ||
|
||
# XXX: instead of manually figuring out out passes are to be batched |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# XXX: instead of manually figuring out out passes are to be batched | |
# XXX: instead of manually figuring out how passes are to be batched |
@@ -0,0 +1,2450 @@ | |||
## `vmir`-based C code-generator. Separated into two phases: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like how conceptually simple this is. : 🎉
let scope = p.scopes.pop() | ||
p.activeLocals.setLen(scope.firstLocal) | ||
|
||
# XXX: the emission of ``ntkLocEnd`` instructions is disabled for now. When |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still reading/understanding how this should work, but from a quick glance, the following occurred to me. Since we know there will be a start and stop for each could we:
- preallocate two sequences of length equal to the number of locals (could also be a tuple)
- location order and sequence order must match
- each start/end has a positional offset, treat the offset as the instruction being appended with a ntkLocStart/ntkLocStop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tangentially, it occurs to me that perhaps a bunch of this dramatically simplifies thanks to CPS + structuring, as local lifetime cannot exceed the lifetime of the continuation, unless passed on (move/copy). The CPS transform should be during the semantic analysis phase as it'll change meaning for a number of things. But that should ease this nonetheless as the shape of "procs" you receive will be very small and regular.
Oh well, that's all one fine day right now. 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since scopes (in the context of lifetimes) aren't directly encoded in the IR, the original idea was to use ntkLocEnd
as a way to signal the end-of-life of a location on a control-flow path. This information was meant to be used by the move-analyser and related data-flow analysis, as well as the code-generator for the VM (to simplify register allocation).
Because each local can have more than one associated ntkLocEnd
instruction, the sequence idea wouldn't work. Attaching extra out-of-band information to instructions is also a bit problematic right now, as the attachment position have to be adjusted separately on each code modification.
Aside: Instructions might need a stable ID for other things, but because of the additional memory they'd take up, I'm still trying to get around having to introduce them.
With the change of plan (i.e. the injectdestructors
rewrite that I'll be working on first), most of the ideas around ntkLocEnd
have become obsolete/stale.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for through explanation.
After coming back to this after some time, I think I get it more than I did before. ntkLocEnd
is effectively acting as a "consume" instruction being automatically inserted by the compiler. This needs to be branch aware at present... but also leads me back to thinking it simplifies under CPS. 😅
But don't worry about all that jazz, it's just me thinking out loud.
mapped: seq[TypeId] ## ``IRIndex`` -> mapped type of the expression. A | ||
## mapped type is the type after lowering/transformation. | ||
|
||
TypedPass*[T] = object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TypePassVistor
var b: CAstBuilder | ||
b | ||
|
||
template buildAst(code: untyped): CAst = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think
start().....fin()
or fin: start()....
is better than buildAst: builder....
as it doesn't require knowledge of another template and its builder
variable.
@@ -146,22 +148,27 @@ proc commandCompileToC(graph: ModuleGraph) = | |||
let conf = graph.config | |||
extccomp.initVars(conf) | |||
semanticPasses(graph) | |||
if conf.symbolFiles == disabledSf: | |||
if conf.symbolFiles == disabledSf and not isDefined(graph.config, "cbackend2"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be flipped to
if isDefined(graph.config, "cbackend2"): ...
elif conf.symbolFiles == disabledSf: ...
as you did in line 169.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that's cleaner, thanks.
Update: I'm currently in the process of splitting off (iterated upon versions of) the various bits developed here into separate PRs, as detailed by the plan described here. It's likely that this PR itself is never going to be merged, but I'm leaving it open for now, since some of the discussions here are still relevant. |
This PR is outdated and has been superseded by #1333. |
Summary
This PR implements:
vmir
,irtypes
,irliterals
transf
AST to the IRs -irgen
,irtypes
,cbackend2
irpasses
,cpasses
,typeinfogen
,markergen
,typeprocessing
cbackend2
,cgen2
The overall goal is to design and implement a simple, modular, and data-oriented framework for developing the various back-ends/target with.
The
README.md
that exists is outdated - a lot of things changed since it was written. It still acts as a good introduction to the code however.Notes for Reviewers
The PR is quite large. I'd suggest starting with looking at
cbackend2.generateCode
, and after getting familiar with how it works on a high-level, continue with the components/sub-systems it uses.There are a lot of maximum line-length violations (especially in code using the
IrCursor
API). For the most part, these are in parts of the code where I'm not yet happy with the general architecture, and that are thus likely to be rewritten anyway.I also left a lot of annotations regarding ideas, to-dos, questions, and general problems in the code - feel free to comment on them.
The documentation very likely needs some extra attention. Not all routines are documented yet, and for those that are, I believe that the documentation is sometimes either glossing over too many details or focusing on the wrong things.