added generation of type specific 'MarshalJSON' & 'UnmarshalJSON' implementations#98
added generation of type specific 'MarshalJSON' & 'UnmarshalJSON' implementations#98sailingKieler wants to merge 16 commits into
Conversation
| } | ||
| // We expect at this point that all references have been attempted to resolve. | ||
| if !r.resolved.Load() { | ||
| return nil, errors.New("reference not resolved") |
There was a problem hiding this comment.
Proposal: generate a partial reference like in Langium:
There was a problem hiding this comment.
This is just preliminary, I don't know which context instance to rely on for calling Ref(...), yet
(since this function is called by json.Mashall(...), not by ourselves), and whether we should attempt to resolve here, at all.
AFAICS, r.resolved is true if resolution has been attempted, regardless of success or failure.
Hence, this guards stops the serialization, e.g., if the document hasn't been build until DocStateLinked at least.
| RefText string `json:"refText"` | ||
| URI string `json:"uri,omitempty"` | ||
| Err string `json:"error,omitempty"` | ||
| }{ |
There was a problem hiding this comment.
This struct def should be exported. And I propose aligning this with Langium for cross-framework compatibility:
There was a problem hiding this comment.
This struct def should be exported.
Why?
It's purely technical and kind of a vehicle.
It might make sense to rely in the two (Un)Marshal methods on the same definition, though.
And I propose aligning this with Langium for cross-framework compatibility:
That's my plain, too.
| Err string `json:"error,omitempty"` | ||
| }{ | ||
| RefText: r.unit.String(), | ||
| URI: uri, |
There was a problem hiding this comment.
In Langium, we omit the URI if the reference target is in the same document. This can reduce the size of generated JSON by a lot.
Thought: can we include something like a URI converter here as well? Exporting JSONs with absolute file URIs would be pretty useless in many cases.
There was a problem hiding this comment.
In Langium, we omit the URI if the reference target is in the same document. This can reduce the size of generated JSON by a lot.
That's achieved by the omitempty setting. Of course, the prop value shouldn't be set upfront.
Thought: can we include something like a URI converter here as well? Exporting JSONs with absolute file URIs would be pretty useless in many cases.
Yes, that's my plan.
sailingKieler
left a comment
There was a problem hiding this comment.
Thanks for the hints Miro.
This state is a very early one, it doesn't represent the one I have in mind already.
I also seek for a solution that is compatible with Langium, s.t. we could use it as an interface for data exchange
More changes are coming soon :-)
| Err string `json:"error,omitempty"` | ||
| }{ | ||
| RefText: r.unit.String(), | ||
| URI: uri, |
There was a problem hiding this comment.
In Langium, we omit the URI if the reference target is in the same document. This can reduce the size of generated JSON by a lot.
That's achieved by the omitempty setting. Of course, the prop value shouldn't be set upfront.
Thought: can we include something like a URI converter here as well? Exporting JSONs with absolute file URIs would be pretty useless in many cases.
Yes, that's my plan.
| RefText string `json:"refText"` | ||
| URI string `json:"uri,omitempty"` | ||
| Err string `json:"error,omitempty"` | ||
| }{ |
There was a problem hiding this comment.
This struct def should be exported.
Why?
It's purely technical and kind of a vehicle.
It might make sense to rely in the two (Un)Marshal methods on the same definition, though.
And I propose aligning this with Langium for cross-framework compatibility:
That's my plain, too.
| } | ||
| // We expect at this point that all references have been attempted to resolve. | ||
| if !r.resolved.Load() { | ||
| return nil, errors.New("reference not resolved") |
There was a problem hiding this comment.
This is just preliminary, I don't know which context instance to rely on for calling Ref(...), yet
(since this function is called by json.Mashall(...), not by ourselves), and whether we should attempt to resolve here, at all.
AFAICS, r.resolved is true if resolution has been attempted, regardless of success or failure.
Hence, this guards stops the serialization, e.g., if the document hasn't been build until DocStateLinked at least.
23336ec to
10ec511
Compare
f72abbc to
cc2a141
Compare
cc2a141 to
8ca6a6c
Compare
|
Just an idea: We are only generating a JSON here, right? |
Lotes
left a comment
There was a problem hiding this comment.
Here are some comments. In the reference part I was not so deep.
I hope it helps :-) ...
About the mentioned "embed trick", I am not sure whether there are downsides. Using it within the tests should be fine.
|
|
||
| func GenerateJSON(grammar grammar.Grammar, packageName string) string { | ||
| node := codegen.NewNode() | ||
| node.AppendLine("// Code generated by typefox.dev/fastbelt/cmd/fastbelt. DO NOT EDIT.") |
There was a problem hiding this comment.
We could move this line to a constant that is shared over all generators.
There was a problem hiding this comment.
Actually, this is AI generated code.
In case we need to update it we wanna prompt a coding agent to update it consistently, don't we? 😉
| builder := service.MustGet[workspace.Builder](f.Services()) | ||
|
|
||
| t.Run("selfContained", func(t *testing.T) { | ||
| t.Cleanup(cleanup) |
There was a problem hiding this comment.
In my eyes it looks a bit weird that you clean up before doing something.
defer t.Cleanup(cleanup) would clean up after the test was processed.
There was a problem hiding this comment.
From the docs of *common.Cleanup(...):
Cleanup registers a function to be called when the test (or subtest) and all its subtests complete.
Thus, it is supposed to do what we expect, i.e., do clean up once the test is completed.
| }) | ||
| } | ||
|
|
||
| const selfContainedJson = `{ |
There was a problem hiding this comment.
We could use embedded files here.
Then the "code space" gets not polluted with big escaped JSON strings.
But I am also fine like it is now. Just wanted to show an opportunity to save > 200 LOC that are actually a different language.
There was a problem hiding this comment.
As discussed in the meeting, it won't be super beneficial here but should keep that opportunity in mind for more complex tests.
| var stringListFields = map[string]string{} | ||
| for _, field := range fields { | ||
| if field.Array && (field.GType == TOKEN_TYPE || field.GType == COMPOSITE_TYPE) { | ||
| varName := strings.ToLower(field.Name[:1]) + field.Name[1:] |
There was a problem hiding this comment.
Move out repeating snippet like name concatenation to a function.
Or may collect frequently used variable names, instead of computing them in place.
For: varName, jsonTag.
Also because you do not have to change N places if you decide that the names shall change.
There was a problem hiding this comment.
Sourced strings.ToLower(field.Name[:1]) + field.Name[1:] out to the composition of the FieldInfo description data in type_generator.go.
| } | ||
|
|
||
| func generateDispatchingUnmarshalFunc(node codegen.Node, g grammar.Grammar) { | ||
| node.AppendLine(`// Unmarshal decodes data into an instance of type T by reading the "$type" field,`) |
There was a problem hiding this comment.
This function outputs a string that differs only in the grammar name.
I am wondering if we can generate this part in a less verbose way.
Actually it is like a utility function that could almost live in a separated file besides the generated functions. Can we move it out the generated folder? The advantage would be that you get displayed syntax errors for free.
There was a problem hiding this comment.
Indeed, it is basically a utility function, and I had it in the framework first.
However, it appeared fairly orphaned there with almost no context, so I added it to the generated code, also because it's called in the generated code.
This way it will always be consistent with the generated code.
It's a matter of taste.
There was a problem hiding this comment.
I'm in favor of moving this function to the framework by expecting the syntheticFactories map as an argument.
| } | ||
|
|
||
| func generateFunctions(grammar grammar.Grammar, node codegen.Node) { | ||
| node.AppendLine(("func newToken(tokenType *core.TokenType, view string) *core.Token {")) |
There was a problem hiding this comment.
This utility function does not differ from other generations. It would make sense to move it outside the generated folder.
There was a problem hiding this comment.
It also a utility that is tightly coupled to its callers that we might change depending on the open question regarding the token type, therefore it's non-exported.
In the face of #128 we might even drop it and inline its impl.
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| grammmar, err := os.ReadFile(tt.path) |
There was a problem hiding this comment.
Think of using the embed trick from above?
There was a problem hiding this comment.
I though about this for a moment, but it doesn't really make sense to me because of:
- this is a test, not production to be bundled into a self-contained executable.
- I would have to create a string var for each file to embed.
- I want the execution to fail with an ordinary test failure instead of some compiler error if any file is absent/any path is wrong
| // assertEqualAst recursively compares two AST subtrees node by node. Diffs are | ||
| // reported via t.Errorf with the node's document path from core.PathOf. | ||
| // Returns true when at least one difference was detected. | ||
| func assertEqualAst(t testing.TB, expected, actual core.AstNode) bool { |
There was a problem hiding this comment.
It looks cleaner if these helper function would live in a own file.
But I think it is ok FTM, as long as there is only one test file that uses them.
There was a problem hiding this comment.
Moved it into a separate file named test/doc_fixture_util.go, as the function seems to be too long & complex to add it to one of the existing files in test.
| // 'err' contains the error msg if the reference was unresolvable before serializing | ||
| // With this function Reference[T] implements json.Unmarshaler.UnmarshalJSON() and the method is called by `json.Unmarshal()`. | ||
| func (r *Reference[T]) UnmarshalJSON(data []byte) error { | ||
| aux := &struct { |
There was a problem hiding this comment.
You should make a private type here, because it is repeating.
There was a problem hiding this comment.
That private type will be visible in the entire Fastbelt core package, which doesn't make any sense.
Yes we could chose a name the makes misuses obvious but, honestly, I don't see any benefit in that.
…lementations * also added generic 'MarshalJSON' & 'UnmarshalJSON' implementation for 'Reference'
631fd58 to
666503a
Compare
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.
| Benchmark suite | Current: c7832b9 | Previous: d803417 | Ratio |
|---|---|---|---|
BenchmarkWorkspaceCycle (typefox.dev/fastbelt/examples/statemachine) - MB/s |
13.02 MB/s |
5.21 MB/s |
2.50 |
This comment was automatically generated by workflow using github-action-benchmark.
84e47c7 to
c1e8c43
Compare
c1e8c43 to
c7832b9
Compare
spoenemann
left a comment
There was a problem hiding this comment.
Great! I like the overall approach.
| // This program and the accompanying materials are made available under the | ||
| // terms of the MIT License, which is available in the project root. | ||
|
|
||
| package fastbelt |
There was a problem hiding this comment.
The package name is inconsistent with the containing folder name. In addition, json is not a good package name because it conflicts with the standard library package. As this file currently exports only one function, I suggest moving it to the root of this module.
| // UnmarshalAndBuildDocument uses the "encoding/json" entry point to unmarshal rootNode based on the given data string and builds the document. | ||
| // Similar to parsing-based document loading the ast build up first including the reference, while the resolution of the references is done during the linking phase of the building process. | ||
| // For properly linking references to other documents, a helper object is attached to the context given to builder providing access to other documents. | ||
| func UnmarshalAndBuildDocument[T core.AstNode](sc *service.Container, document *core.Document, rootNode T, data []byte, ctx context.Context) error { |
There was a problem hiding this comment.
The ctx parameter should be the first per context package convention.
Wouldn't it be more useful to split the "Unmarshal" and the "Build" part into separate utilities? The former could go to the fastbelt (root) package, the latter to the workspace package.
There was a problem hiding this comment.
Actually, my intention is to give a blue print how to do unmarshaling with cross-ref resolution.
We could move it into the docs, too, but then it's not compile checked anymore.
Therefore, I was hesitant to put it into the root package and pollute the namespace with these nice-to-have parts.
I'm fine with putting into util.
| // in case a document in the document manager hasn't been build yet or has been reset | ||
| // and is not included in the current build cycle its exported symbols container may be absent; | ||
| // map 'nil' to an empty seq in such cases | ||
| return func(yield func(*SymbolDescription) bool) {} |
There was a problem hiding this comment.
We have EmptySymbolDescriptions for this. BTW shall we rename that to EmptySymbolSeq to be consistent with the SymbolSeq type?
There was a problem hiding this comment.
BTW shall we rename that to EmptySymbolSeq to be consistent with the SymbolSeq type?
Makes sense IMHO.
| return nil | ||
| } | ||
|
|
||
| type JsonLinkingHelper interface { |
There was a problem hiding this comment.
I'd consider making all these helper definitions private, including the NewJsonReferenceGetter function below. We can still make them public later if we see legitimate use cases.
In any case, please add some documentation to explain how these things work together.
There was a problem hiding this comment.
including the NewJsonReferenceGetter function below.
Makes sense, the uppercase initial is probably a leftover.
|
@spoenemann, @msujew One thing I'm still undecided about: fastbelt/internal/generator/type_generator.go Lines 176 to 178 in b789ac2 Then we can call |
Contributes support for exporting and importing ASTs in Json.
Includes:
a generator component producing
json_gen.gofilesjson.Marshaler&json.Unmarshalergeneric
MarshalJSON&UnmarshalJSONimplementations forReferenceutil/json/json.godemonstrates the procedure of importing an AST in json into a Fastbelt containera simple round trip AST export and import test including cross references to other documents (circular) by means of the Arithmetics test language,
a roundtrip AST export and import test (no external cross-refs by now) by means of the Grammar language,
boolean,string,AstNode,Referencex1,?,*,+Note:
fixture.gonilpointer de-reference insymbol.goThere's one known potential issue:
During the import of an AST in json the
Tokeninstances are always initialized with token typeToken_ID.The leads to a compiler issue if there is no
IDtoken type.We could improve this by an analysis of the Parser rules, but that will never be complete.
Imagine the grammar defines interfaces of types that are never instantiated by parser rules, but the data are always imported vom Json snapshots.
To mitigate this we might offer an opportunity in the grammar to determine a token type for fields of type
stringorcomposite. To be discussed.