- How to contribute to Kipper
Welcome to the Kipper contribution guide!
This guide will try to explain the basics of how to contribute to Kipper, how to use issues/PRs and how to modify the source code.
Before starting, thank you for showing interest in this guide! I (@Luna-Klatzer) appreciate any help with this project and am happy to help if there are any questions left unanswered!
One of the first things that must be done before contributing to Kipper is to open an issue, or look at existing issues or pull requests.
- In case you are working on someone else's issue or PR, it is important to read through the details of the issue or PR and get to know what should be done, and how other people proposed to do it.
- In case you are creating your own issue, it is important to think through how your change matters and what they should do. This is vital for others to understand how to work on your issue and give feedback/recommendations.
If you are working on or creating a bug issue, it is important to try and reproduce the issue using tests or sample projects that recreate the situation that the bug was encountered in.
In case you are creating a bug issue, it is also important to provide steps on how to reproduce the issue and info about your environment. The bug issue template will help you
If you are working on or creating a feature issue, it is important to understand the details behind the proposed feature and its possible implementation. If you are going through someone else's issue, it's good to write additional questions, recommendations, ideas or criticism as a comment under the issue.
In case you are creating a feature issue it is also important to provide info on how you exactly want it to work and what your changes will do (More on that in Using PRs to add new changes).
After having gone through an issue and having discussed a change, it is now time to make those changes and try to get them into the future Kipper releases.
Creating and merging changes into the source code is done using pull requests and forks. PRs are vital as they allow for a managed way to propose, document and merge changes into Kipper. That is why it's important to also make sure your PRs are organised, documented well and link related issues, docs or websites. The PR template will help with that.
If you are new to GitHub and PRs, you can follow the guide from GitHub here. to learn how to create such a pull Request.
If you created a new PR, please also update the CHANGELOG.md file and create a Changelog specifically for your PR changes based on the [Keep a Changelog format](Keep a Changelog).
Kipper is split up into multiple packages, which are contained in the main monorepo kipper
that depends on
all Kipper sub-packages.
To work on Kipper you will need to install and use pnpm
, which provides the tools for managing a monorepo like the one
used in this repository.
Pnpm supports, like npm, package.json
scripts, which are heavily used in the process of managing Kipper. It's best
to also use them, since they predefine behaviour for building, testing and other useful stuff.
Overview of important basic scripts:
pnpm test
- Tests Kipper using the files in/test/
.pnpm build
- Builds all Kipper packages in the/kipper/
folder.pnpm start
- Starts the CLI for Kipper. Arguments can be passed usingpnpm start args...
pnpm run browserify
- Builds the browser standalone scriptkipper-standalone.js
.pnpm run antlr4ts
- Builds the Kipper Parser and Lexer using the grammar file/kipper/core/Kipper.g4
pnpm run prettier
- Prettifies and reformats the source and test files.pnpm run lint
- Runs the tslint plugin for eslint and analyses the source code.pnpm run lint:fix
- Runs the tslint plugin for eslint, analyses the source code and automatically tries to fix issues if they are encountered.
The core package is, as already in the name, the core package of Kipper. It contains the lexer, parser, semantic analysers, code translators and the classes and functions making up the Kipper compiler.
The most important class from that package is KipperCompiler
, which provides the user-interface for interacting
with Kipper. It allows the parsing, semantic analysis and compilation of files.
The cli package is the command line interface for interacting with the Kipper compiler using pre-defined commands. It
is not as customisable as if someone would compile code using an imported @kipper/core
package, but it provides an
easy interface to use Kipper and compile code.
To run the Kipper cli in a development environment, you can simply run:
pnpm start ...
The web package is the web interface for interacting with the Kipper compiler. It provides a simple web bundle that can be included in an HTML file and used to generate JavaScript code from Kipper source code.
To generate the bundle file, simply run:
pnpm run browserify
The bundle file will be located in the root folder of the @kipper/web
package (kipper/web
).
The target-ts package is the TypeScript target package for Kipper. It provides the code for translating a Kipper AST into TypeScript code.
The target-js package is the JavaScript target package for Kipper. It provides the code for translating a Kipper AST into JavaScript code.
Kipper is primarily designed to translate to TypeScript code, though currently it is planned to also support other targets, like native JavaScript or AssemblyScript to allow more diverse targets and support a bigger ecosystem.
For the moment though, the only target is TypeScript, which is defined in the file /compiler/target/typescript
of
@kipper/core
. This target also is the default target that will be used for every compilation, unless another target
is specified in CompileConfig
.
The Kipper compiler uses a configuration interface CompileConfig
to configure the compilation of a program. This
interface can be passed as an argument to KipperCompiler.compile()
, where it will be put into a
CompilerEvaluatedOptions
that merges both the default configuration with the user defined configuration.
Currently, configuring KipperCompiler.syntaxAnalyse()
is not supported, as it does not yet support semantic analysis.
This should be implemented in future releases.
If you want to add new functionality for the Kipper compiler, you can easily do that in multiple ways:
- If you want to add new syntax, you will have to edit the Antlr4
/kipper/core/Kipper.g4
file and update theKipperFileListener
, which walks through a generated parse tree and determines what items should be added to theRootASTNode
(represents the root node of the entire file, which contains all top-level statements and declarations as child nodes). - If you want to update the compiler logic and semantics, you will have to work in the
/compiler/semantics
folder of@kipper/core
, where the semantic analyser, type checker and AST node classes are implemented that represent Kipper expressions, declarations and statements. - If you want to update the default translation to TypeScript, you will have to work in the
/compiler/target/typescript
file, which contains the semantic analyser and target code generator for TypeScript. - If you want to work on a new target or add any other functionality, you should add new files that extend the existing functionality.
The semantics of an AST node are represented using an interface that defines what semantic metadata must be present for an instance to be translatable.
The two generic parameters of the CompilableASTNode<Semantics, TypeSemantics>
class are the semantics and type
semantic interfaces, which will hint the type of CompilableASTNode.semanticData
and CompilableASTNode.typeData
.
Usually the semantic interfaces of a Kipper AST node are like this:
export interface AdditiveExpressionSemantics extends ArithmeticExpressionSemantics {
leftOp: Expression;
rightOp: Expression;
operator: KipperAdditiveOperator;
}
export interface AdditiveExpressionTypeSemantics extends ArithmeticExpressionTypeSemantics {
evaluatedType: KipperType;
}
These semantics then are per default processed using the classes implementation of primarySemanticAnalysis()
. This
function should always evaluate and define the semantics by setting the field CompilableASTNode.semanticData
.
Though to avoid unexpected errors, when using semantic data of an AST node they should always be fetched using
CompilableASTNode.ensureSemanticDataExists()
, which throws an error in case they are undefined (This for example
can happen if the child node of a node fails to process and as such the parent can not access the semantic data, since
it was not defined/evaluated).
To update how semantics are handled or what semantic data exists, either the semantics interface or the
function CompilableASTNode.primarySemanticAnalysis()
should be updated and changed.
To assert specific semantics and throw proper errors, you can easily do that using the KipperSemanticChecker
, which is
explained more in-depth here.
As Kipper is a statically and strongly typed language, types must be checked at compile time to ensure the program can execute without issues.
Kipper handles type checking for a single CompilableASTNode
with its primarySemanticTypeChecking()
function implementation, which is called after primarySemanticAnalysis()
. In the function all possible type issues
should be checked for to avoid issues during code generation or execution, as well the CompilableASTNode.typeData
populated to ensure the correct type data is available to other nodes as well that might depend on it.
To assert types and throw proper errors, you can easily do that using the KipperTypeChecker
, which is
explained more in-depth here.
In case that a target (targets are for example TypeScript) has specific semantic logic that must be upheld, a
KipperTargetSemanticAnalyser
is used, which can do additional checks on specific AST nodes. Each program
(KipperProgramContext
) has one KipperCompileTarget
set, which defines how Kipper should be translated. This class
also defines a KipperTargetSemanticAnalyser
, where target-specific semantics may be checked.
To update or add target specific semantic checks, you can update the corresponding functions for the AST node class.
For example KipperTargetSemanticAnalyser.compoundStatement
, which handles the semantic analysis for { }
blocks.
Throwing errors in Kipper is handled similarly to how mocha tests works. A truth is asserted to be true
and if it turns out to be false an error is thrown. This behaviour is handled using the classes KipperSemanticChecker
and KipperTypeChecker
, which pre-define certain assertions that can be performed to validate code.
To also handle tracebacks, any assertion is done using either:
KipperProgramContext.typeCheck
orKipperProgramContext.semanticCheck
which handle the tracebacks by requiring an AST node instance as an argument and returning
a KipperSemanticChecker
or KipperTypeChecker
instance with the proper metadata already set.
This means in case that an assertion fails, these classes will handle the error themselves and
create a KipperError
using the node's metadata.
For example (Code snippet from the class FunctionDeclaration
):
// 'this' is the node class - This may be used in an 'CompilableASTNode'
this.programCtx.typeCheck(this).typeExists(semanticData.returnType);
To update the behaviour of how a Kipper target translates, the target code generator (KipperTargetCodeGenerator
) and
target built-in generator (KipperTargetBuiltInGenerator
) can be updated, which implement the code generation.
In most cases, the KipperTargetBuiltInGenerator
can be left alone, as it implements core and internal functionality
for keywords and internal logic.
For example, the translation behaviour of the TypeScript target can be updated in the
@kipper/core/compiler/targets/typescript
folder.
If you want to make sure your new changes or new functionality works, you will have to add new tests in the
/test/
folder of Kipper.
These tests are categorised into sub-folders per package in the following scheme:
module/
- cli/
- core/
Please add tests for a package to the correlating test folder e.g. make sure CLI tests are in /cli/
and core tests in
/core/
.
Tests are written using mocha and chai, so you can easily add new tests in existing files or new files.
-
To add a new test file, create a new file with the file ending
test.ts
. -
To add a new test namespace, call the
describe
function and pass as argument a new unique name:describe(name, () => { // Add tests here });
-
To add a single test, call
it
inside adescribe
lambda function with a new unique test name:describe(name, () => { it(name, () => { // Use 'async ()' in case you need async functionality }); });
-
To add an assertion/expectation, use
assert(truth);
inside a test.
If you need ideas how to write good tests, look at the exiting ones and try to get an idea what may be important to test.
In case you notice existing tests are insufficient, you can update or rewrite them to make sure everything is tested and the percentage of code covered goes up.