Skip to content
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

Open
wants to merge 396 commits into
base: devel
Choose a base branch
from

Conversation

zerbina
Copy link
Collaborator

@zerbina zerbina commented Aug 31, 2022

Summary

This PR implements:

  • back-end oriented intermediate representations for procedure bodies, types, symbols, etc. - vmir, irtypes, irliterals
  • logic for translating post-transf AST to the IRs - irgen, irtypes, cbackend2
  • transformation and processing logic for the various IRs - irpasses, cpasses, typeinfogen, markergen, typeprocessing
  • a new C back-end based on the components listed above - 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.

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`.
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.
* remove unused types, procedures, and constants
* improve some doc comments
* update/remove/add some annotations
Improve some doc comments, remove stale ones, and add some notes
about future directions/plans.
* 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
Copy link
Collaborator

@haxscramper haxscramper left a 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)
Copy link
Collaborator

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
Copy link
Collaborator

@saem saem Oct 1, 2022

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
Copy link
Collaborator

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.

Copy link
Collaborator

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.

@zerbina zerbina marked this pull request as ready for review October 1, 2022 23:33
Copy link
Collaborator

@saem saem left a 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:

  1. we need to shrink dialect problems in devel
  2. 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:

  1. if you could makes fixes for the things we think should continue to work
  2. 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
Copy link
Collaborator

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))
Copy link
Collaborator

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])
Copy link
Collaborator

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
Copy link
Collaborator

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`
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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``.
Copy link
Collaborator

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.

Copy link
Collaborator Author

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# 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:
Copy link
Collaborator

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
Copy link
Collaborator

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:

  1. preallocate two sequences of length equal to the number of locals (could also be a tuple)
  2. location order and sequence order must match
  3. each start/end has a positional offset, treat the offset as the instruction being appended with a ntkLocStart/ntkLocStop

Copy link
Collaborator

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. 😅

Copy link
Collaborator Author

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.

Copy link
Collaborator

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
Copy link
Collaborator

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 =
Copy link
Contributor

@Clyybber Clyybber Oct 9, 2022

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"):
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

@zerbina zerbina removed the request for review from alaviss October 14, 2022 19:28
@haxscramper haxscramper added refactor Implementation refactor compiler General compiler tag compiler/backend Related to backend system of the compiler labels Nov 20, 2022
@haxscramper haxscramper added this to the C backend refactoring milestone Nov 21, 2022
@zerbina
Copy link
Collaborator Author

zerbina commented Feb 22, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler/backend Related to backend system of the compiler compiler General compiler tag refactor Implementation refactor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants