diff --git a/README.md b/README.md index f4275505..a5c0feca 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ A general-purpose dataflow programming language with static types and implicit p ## Features 🚀 - **Dataflow Programming** -- **Strong Static Typing** - **Implicit Parallelism** - **Compiles to Machine Code, Go and More** -- **Clean C-like Syntax** - **Garbage Collection** +- **Strong Static Typing** +- **Clean C-like Syntax** - **...And more!** -Note: Features are implemented but may have poor developer experience. No backward-compatibility guaranteed during development. +_Note: Features are implemented but may have poor developer experience. No backward-compatibility guaranteed at the moment._ ## Quick Start @@ -109,4 +109,3 @@ Also please check our [CoC](./CODE_OF_CONDUCT.md). ## Contributing See [CONTRIBUTING.md](./CONTRIBUTING.md) and [ARCHITECTURE.md](./ARCHITECTURE.md). - diff --git a/docs/README.md b/docs/README.md index 77b4a62a..7d17089e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,25 +1,25 @@ -# Documentation +# Nevalang Documentation -Welcome to Nevalang's documentation! - -It describes general purpose dataflow compiled language with static types. Here you'll find motivation behind the language, it's philosophy, differences between dataflow and controlflow paradigms and of course language abstractions and relations between them. +This document provides an overview of Nevalang, its core concepts, and how to use it effectively. ## About this Document -1. This document describes finite state of the language. Some features (e.g. visual editor) might not be implemented at the time of writing, but they yet important for concept so they will be mentioned -2. It was written as an attempt to create at least something. I needed to dump all the information I collected about the language with years. So it's far from perfect -3. This document doesn't teach you how to write Nevalang programs, because Nevalang is very immature language and it is changins all the time. This is especially true for stdlib components. However, a lot about Nevalang is already clear and will never change. We are talking about more fundamental stuff such as philosophy, abstractions, execution model, etc. +1. Covers conceptual features, some not yet implemented +2. Compiles initial language information +3. Focuses on fundamental aspects rather than specific APIs -## Table of contents +## Table of Contents -- [About](./about.md) -- [Motivation](./motivation.md) -- [Paradigm](./paradigm.md) - - [Flow-Based-Programming](./fbp.md) -- [Program Structure](./program_structure.md) - - [Type](./type_entity.md) - - [Constant](./const_entity.md) - - [Interface](./interface_entity.md) - - [Component](./component_entity.md) - - [IO](./component_io.md) - - [Network](./component_net.md) +1. [About](./about.md) + 1. [Implicit Parallelism](./other.md#implicit-parallelism) + 2. [Visual Programming](./other#visual-programming-wip) + 3. [Flow-Based Programming](./other#flow-based-programming) + 4. [Structural Subtyping](./other.md#structural-subtyping) +2. [Program Structure](./program_structure.md) + 1. [Types](./types.md) + 2. [Constants](./constants.md) + 3. [Interfaces](./interfaces.md) + 4. [Components](./components.md) + - [Networks](./networks.md) +3. [Style Guide](./style_guide.md) +4. [FAQ](./faq.md) diff --git a/docs/about.md b/docs/about.md index e08bf717..0cf6b171 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,67 +1,155 @@ # About -Nevalang is a general purpose programming language: documentation, compiler, runtime, cli, editor plugins, etc. What kind of language it is? +Nevalang is a general-purpose programming language with the following key characteristics: -- Pure Dataflow (FBP-like) -- Compiled (Machine code, Golang, WASM) -- Statically (Strongly) Typed -- Concurrent/Parallel by default -- Hybrid (textual/visual) -- Pure Declarative -- Go Interop (call Neva from Go and vice versa) +- **Pure Dataflow**: Programs are expressed using dataflow abstractions like nodes, ports, connections, and messages. Traditional programming constructs (variables, functions, loops, etc.) are absent. +- **Implicit Parallelism**: Parallelism is built-in, allowing concurrent execution without explicit primitives like mutexes or threads. +- **Compilation**: Nevalang compiles to Go as an intermediate representation, then to machine code or WASM, offering performance benefits and easier deployment. +- **Strong Static Typing**: The language employs a strong, static, and structural type system, ensuring type safety at compile-time. +- **Hybrid Programming (WIP)**: Nevalang supports both textual and visual programming as first-class citizens, allowing flexibility in development approaches. +- **Go Interoperability (WIP)**: Nevalang can interact with existing Go codebases, allowing for gradual adoption and integration with Go projects. -We will discover what this means what piece by piece. +These features combine to create a unique language that emphasizes dataflow, type safety, and declarative programming while leveraging Go's ecosystem and compilation targets. Nevalang aims to provide a fresh approach to programming, particularly suited for concurrent and parallel applications. -## Pure Dataflow +## Motivation -Pure Dataflow means that your program is expressed in terms of dataflow abstractions such as nodes, ports, connections and messages. +Nevalang aims to create a better programming language where programs are easier to understand, debug, and create. Here are the key motivations behind its development: -There are no variables, no functions, no classes, no objects, no methods, no loops, no ifs, no switches, no exceptions, no mutable state. +### General Purpose Dataflow -Nevalang's dataflow mostly influenced by Flow-Based Programming paradigm but Nevalang is not pure FBP. You can find more info about this in specialized section. +Most general-purpose languages are controlflow-based, while dataflow languages are often domain-specific. Nevalang seeks to bridge this gap by offering a pure, general-purpose dataflow language with static typing. -## Compilation +### Implicit Parallelism -Nevalang uses Go as low-level IR (Intermediate Representation). This means your Nevalang program is firstly compiled to Go and then Go compiler is used to compile it to any target that Go compiler supports, such as machine code or WASM. +Nevalang introduces _implicit parallelism_, automating concurrency control like garbage collection automated memory management. Key features: -This firstly has performance benifits and secondly it's easier to deploy if all you need to ship is a binary, unlike Python or Java where you need interpreter/VM of a specific (correct) version. +- Components exchange messages via buffered queues +- Blocking limited to specific parts, allowing others to continue +- First-class stream processing for efficient data handling +- No low-level concurrency primitives, enhancing safety -There's a temptation to say that Nevalang is as fast as Go but that statement is incorrect because Nevalang is higher abstraction. It has it's own (very-very thin, but still) runtime on top of Go's. +This approach makes concurrent programming the default, simplifying development. -## Strong Static Typing +### Text/Visual Hybrid (WIP) -Nevalang has _strong static structural_ type-system. What does it mean? +Despite humans' visual nature, text-based coding has dominated for decades. A general-purpose visual programming language is needed, as existing visual languages lack the popularity of text-based ones. -- _Strong_ - means there are no implicit type-casts. E.g. `float` will never be implicitly converted to `int` and you will always have to convert (cast) them explicitly, by using special components -- _Static_ - means that types are known at compile time. E.g. - you can (and you must) explicitly define what data type your port is sending or receiving, or what data-type some constant have -- _Structural_ - means _structural sub-typing_ is used. In this case it used for data-types and interfaces/components implementation relation. +Visual programming's necessity is evident in software design diagrams and the popularity of visual tools. People naturally think in interconnected processes, yet static diagrams quickly become outdated. -If you are confused by something, don't worry. Type-system will be explained in detail on related section. +The claim that visual programming is less maintainable is unfounded. Nevalang offer abstraction similar to text-based programming through modules, packages and components. -## Implicit Parallelism +Nevalang aims to combine textual and visual programming in the future, addressing the limitations of purely visual languages. The plan is to implement a minimalistic C-like syntax that can be fully represented visually, making it compatible with text-based tools like version control systems. -Nevalang supports implicit parallelism. It means that you don't have to (and you can't) access parallelism/concurrency primitives such as mutexes, threads, coroutines, channels, etc. You just create dataflow and everything that can happen in parallel (better word is _concurrently_) - will happen this way. It's good because of 2 things: performance and maintainance. You basically get parallelism for free. +### Optimized Type System -If you want to know how exactly Nevalang executes your program then don't worry, there will be a special section about that. This is just a high level overview of a specific feature. +Nevalang aims for a balanced type system that aids developers without being overly restrictive. It combines elements from Go and TypeScript, featuring structural subtyping for data types and interfaces, along with generics with constraints. -## Hybrid (Textual and Visual Programming) +#### Structural Subtyping -Textual and visual programming in Nevalang are combined into single workflow. You can use only text if you e.g. work in environment that doesn't support visuals or you just don't like visual programming. However, Nevalang is and always will be hybrid langauge, which means visual programming is a first class citizen. If some feature cannot be supported in visual programming it might not be added or can be removed. Even language abstractions are done in a way so it's possible to visually represent them. +Unlike Go’s nominative subtyping, Nevalang checks type compatibility based on structure, not name, avoiding unnecessary casting. In Go, a `readBook` function taking a `Book` struct won’t accept a `Magazine` struct, even though `Magazine` has the necessary fields. -## Pure Declarative +```go +type Book struct { title string, author string } +type Magazine struct { title string, author string, number int } +func main() { + readBook(Magazine{title: "FBP time", author: "Emil Valeev"}) // compile error! +} +func readBook(book Book) { fmt.Println(book) } +``` -Nevalang doesn't have a single imperative bit except go-interop (including both writing extensions and maintaining stdlib). +Go requires explicit casting: -Declarative Programming means you write your program by describing what do you want to get as a result, instead of explaining how to actually do it under the hood. E.g. explicit controlflow instructions like `for`, `break` or `goto` are imperative while things like `[1,2,3].map(double)` are declarative. You are not explaining _how_ to actually `map` stuff, you just say that this is _what_ result should be. +```go +readBook(Book{ + title: magazine.title, + author: magazine.author, +}) +``` -In Nevalang declarative programming is expressed in a way you write your program - you define entities. Computation is expressed in terms of graph that is also declarative. Network show what should happen (e.g. message from X should be moved to Y) and not how (send message to channel, block coroutine, etc). +In Nevalang, structural typing eliminates this problem. For example, in web apps using API boundaries like gRPC or GraphQL, where developers often write mapping code between generated types and domain models, structural typing automatically handles compatibility, avoiding hundreds of lines of manual conversions. -Of course there are different problems and imperative programming is better for some of them. However, if we have imperative languages, why not have declarative? Besides, there are tons of declarative languages, they are just (mostly) not dataflow. Let's take functional programming paradigm for instance. If we are talking about pure FP then we are automatically talking about pure declarative. Same goes for dataflow - pure dataflow is always pure declarative. +### Better Debugging -## Go Interop +#### Improved Error Handling -As being said Nevalang uses Go as low-level IR which opens the door for two-way interop with existing Go codebases. That is, Nevalang has potential to call Go code from neva code and vice versa - call Nevalang from Go. +Following Go's "errors are values" approach, Nevalang treats errors as data types. It incorporates Rust-like error handling with a `?` operator, while ensuring that errors are always handled when present. -This is very important thing about Nevalang - it's intended for gradual adaptation among Go developers. Otherwise it would be close to impossible to adapt the language to such a highly competitive market. +#### Advanced Tracing -However, if you are not Go developer, don't worry. Nevalang won't have interop with your language so you will have to use network for communication. However, it's completely fine to use Nevalang without knowing anything about Go. Nothing requires you Go knowledge to write Nevalang programs. Maybe couple of things might not seem obvious without Go background or as a non-Go developer you couldn't appreciate some decisions. That doesn't matter. Nevalang is separate language. Nevalang to Go is more what C to assembler is, rather than TypeScript to JavaScript. +Every message in Nevalang has a path that updates as it moves through the program. This provides comprehensive tracing capabilities, similar to stack traces in exception-based languages, but for all messages, not just errors. + +#### Next-Generation Debugging (WIP) + +The combination of dataflow architecture and advanced tracing enables powerful debugging tools. Developers can set visual breakpoints on specific connections in the network graph, observe messages, and even update their values during runtime. + +By combining these features, Nevalang strives to offer a more efficient and intuitive programming experience, pushing the boundaries of what's possible in language design. + +## Paradigm + +Nevalang adopts the dataflow paradigm, which explains computation in terms of directed graphs. While influenced by Flow-Based Programming (FBP), Nevalang [diverges](./other/fbp.md) from it in several ways. + +### Dataflow vs Controlflow + +Two high-level programming paradigms exist: Dataflow and Controlflow. Dataflow includes variations like Actors and CSP, while Controlflow encompasses OOP and FP. + +Dataflow programming typically involves nodes, connections, and asynchronous message passing. Some controlflow languages (e.g., Go, Erlang) support dataflow subsets, but combining the two paradigms can be challenging. + +### Nevalang's Approach + +Nevalang makes dataflow its primary paradigm, with controlflow support as a secondary feature. Key dataflow concepts in Nevalang include: + +- Components and Nodes +- Ports (input/output) and Connections +- Immutable Messages + +Unlike many controlflow languages, Nevalang strictly separates code and data. Messages, not components, are passed between components. + +### Dependency Injection + +Nevalang supports polymorphism through interfaces and static dependency injection. While this approach is familiar to controlflow programmers from statically typed languages, Nevalang's implementation is purely static due to the separation of code and data. + +### Declarative Nature + +Nevalang is a declarative dataflow language, focusing on "what" rather than "how". This approach contrasts with imperative languages and even declarative controlflow languages like Haskell, which still use controlflow concepts like call/return. + +### Advantages and Challenges + +Dataflow excels in visualization and parallelism but can struggle with enforcing strict order of operations. While controlflow benefits from its long-standing dominance and extensive resources, dataflow presents opportunities for research and innovation in programming paradigms. + +### Flow-Based Programming + +> Feel free to skip this section if you are not familiar with the original concept of fbp. + +Nevalang's dataflow is influenced by FBP but differs in several key aspects: + +#### Granularity and Controlflow + +Nevalang is designed for general-purpose programming, expecting entire programs to be written in dataflow, unlike FBP which is used for high-level orchestration. Nevalang provides low-level components (written in Go) for operations like math, eliminating the need for users to write controlflow code except when contributing to the stdlib or integrating with Go. + +#### Data Handling + +Nevalang is garbage-collected with immutable data, avoiding ownership concepts. This prevents data races but may impact performance. Mutations are possible via unsafe packages (WIP) but are discouraged. FBP, in contrast, uses ownership and allows mutations. + +#### Node Behavior + +Nevalang's nodes are always running, automatically starting, suspending, and restarting as needed. FBP processes have explicit states (start, suspend, restart, shutdown) that can be manipulated. + +#### Static Typing + +Nevalang features a static type system with generics and structural sub-typing, improving IDE support and reducing runtime validations. FBP is dynamically typed in its dataflow part. + +#### Similarities + +Both paradigms support dataflow and implicit parallelism, sharing much terminology. + +#### Terminology Comparison + +| FBP | Nevalang | +| -------------------------------- | ------------------------- | +| Component (Atomic/Complementary) | Component (Native/Normal) | +| Process | Node | +| Connection | Connection | +| Ports | Ports | +| IP | Message | +| IIP | Constant | +| IP Tree | Structure or Dictionary | diff --git a/docs/component_entity.md b/docs/component_entity.md deleted file mode 100644 index 9d9ac46e..00000000 --- a/docs/component_entity.md +++ /dev/null @@ -1,63 +0,0 @@ -# Component Entity - -Component always has signature (basically embedded _interface_) and optional _compiler directives_, _nodes_ and _network_. There are two kinds of components: _normal_ and _native_ ones. - -## Main Component - -Executable package must have _component_ called `Main`. This component must follow specific set of rules: - -- Must be _normal_ -- Must be _private_ -- Must have exactly 1 inport `start` -- Must have exactly one outport `stop` -- Both ports must have type `any` -- Must have no _abstract nodes_ - -Main component must have both node and network.jkl - -## Native Components - -Component without implementation (without nodes and network) must use `#extern` directive to refer to _runtime function_. Such component called _native component_. Native components normally only exist inside `std` module, but there should be no forced restriction for that. - -## Normal Component - -Normal component is implemented in source code i.e. it has not only interface but also nodes and network, or at least just network. Normal components must never use `#extern` directive. - -## Nodes - -Nodes are things that have inports and outports that can be connected in network. There's two kinds of nodes: - -1. IO Nodes -2. Computational nodes - -IO nodes are created implicitly. Every component have one `in` and one `out` node. Node `in` has outports corresponding to component's interface's inports. And vice versa - `out` node has inports corresponding to component interface's inports. - -Computational nodes are nodes that are instantiated from entities - components or interfaces. There's 2 types of computational nodes: concrete and abstract. Nodes that are instantiated from components are _concrete nodes_ and those that instantiated from interfaces are _abstract nodes_. - -Interfaces and component's interfaces can have type parameters. In this case node must specify type arguments in instantiation expression. - -## Dependency Injection (DI) - -Normal component can have _abstract node_ that is instantiated from an interface instead of a component. Such components with abstract nodes needs what's called dependency injection. - -I.e. if a component has dependency node `n` instantiated with interface `I` one must provide concrete component that _implements_ this interface. - -Dependency Injection can be infinitely nested. Component `Main` cannot use dependency injection. - -## Component and Interface Compatability (Implementation) - -Component _implements_ interface (is _compatible_ with it) if type paremeters, inports and outports are compatible. - -Type parameters are compatible if their count, order and names are equal. Constraints of component's type parameters must be compatible with the constraints of the corresponding interface's type parameter's constraints. - -Component's inports are compatible with the interface's if: - -1. Amount is exactly equal -2. They have exactly the same names and _kind_ (array or single) -3. Their types are _compatible_ (are _subtypes_ of) with the corresponding interface's inports - -Outports of a component are compatible with the interface's if: - -1. Amount is equal or more (this is only difference with inports) -2. Exactly the same names and _kind_ -3. Their types are _compatible_ diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 00000000..0627c174 --- /dev/null +++ b/docs/components.md @@ -0,0 +1,49 @@ +# Components + +Components have a signature (interface), optional compiler directives, nodes, and network. They can be normal or native. + +## Main Component + +Executable packages must have a private `Main` component with: + +- One `start` inport and one `stop` outport, both of type `any` +- No interface nodes +- Both nodes and network +- Not public + +## Native Components + +Components without implementation use `#extern` directive to refer to runtime functions. Typically found in `std` module. + +## Runtime Function Overloading + +Native components can use overloading with `#extern(t1 f1, t2 f2, ...)`. Requires one type parameter of type `union`. + +## Normal Component + +Implemented in source code with network and maybe nodes. Must not use `#extern` directive. + +## Nodes + +Nodes are instances of other components that can be used in component's network to perform computation. + +If entity that node refers to (component or interface) have type-parameters, then type-arguments must be provided with node instantiation. If node is instantiated from component that requires dependencies, then other node instantiations must be provided as those dependencies. + +There are 2 types of nodes: + +1. IO Nodes - `in` and `out`, created implicitly, you omit node name when you refer to them (e.g. `:start`, `:stop` instead of `in:start` and `in:stop`) +2. Computational nodes - explicitly created by user, instances of entities: + - Components (concrete/component nodes) + - Interfaces (abstract/interface nodes) + +## Dependency Injection (DI) + +Normal components can have interface nodes, requiring DI. `Main` component cannot use DI. + +## Component and Interface Compatibility + +A component implements an interface if: + +- Type parameters are compatible (count, order, names, constraints) +- Inports are compatible (equal amount, same names/kind, compatible types) +- Outports are compatible (equal or more amount, same names/kind, compatible types) diff --git a/docs/const_entity.md b/docs/constants.md similarity index 82% rename from docs/const_entity.md rename to docs/constants.md index f80236e2..117bb3ff 100644 --- a/docs/const_entity.md +++ b/docs/constants.md @@ -1,3 +1,7 @@ -# Constant Entity +# Constants Constant is an entity that consist of either _message_ or _entity reference_ to other constant. Message can include references to other constants. Constant messages can be infinitely nested. Constants may refer imported constants from other packages. _Components_ are only entities that can refer constants, that are not constants themselves - they can refer to constants via _compiler directives_ and from their _networks_. + +## Bound Constant + +Constant that is referenced inside `bind` compiler directive diff --git a/docs/data_types.md b/docs/data_types.md deleted file mode 100644 index f76b0f98..00000000 --- a/docs/data_types.md +++ /dev/null @@ -1,75 +0,0 @@ -# Data Types - -Nevalang has strong static structural type-system - -- **Strong** means that there's no implicit type convertions -- **Static** means that semantic analysis for type-safety happens at compile time, not run-time -- **Structural** means that types are compatible if their structure is compatible. Unlike nominative sub-typing, names of the types does not matter. This also means that type can carry more information than needed and still be compatible. - -## Base Types - -These are types that doesn't have definition in neva source code. Instead compiler is aware of them and knows how to handle them: - -- _any_ -- _maybe_ -- _bool_ -- _int_ -- _float_ -- _string_ -- _dict_ -- _list_ -- _enum_ -- _union_ -- _structure_ - -## Any (Top-Type) - -`any` is a _top-type_. It means that any other type is a _sub-type_ of any. That means you can pass any type everywhere `any` is expected. However, since `any` doesn't tell anything about the type, you cannot pass message of type `any` anywhere where more concrete type is expected. You need to either rewrite your code without using any or explicitly cast `any` to concrete type. - -## Maybe - -Maybe represents value that maybe do not exist. One must unwrap maybe before using the actual value. - -## Boolean - -Boolean type has only two possible values `true` and `false` - -## Integer - -Integer is 64 bit integer number - -## Float - -Integer is 64 bit floating point number - -## Strings - -Strings are immutable utf encoded byte arrays - -## Maps - -Maps are unordered key-value pairs with dynamic set of keys. All values must have the same type - -## List - -List is a dynamic array that grows as needed. All values in the list must have the same type. - -## Enums - -Enums are set fixed set of values (members) each with its own name. They are represented in memory like integer numbers. - -## Union - -Union is a _sum type_. It defines set of possible types. - -## Struct - -Structures are product types (records) - compile-time known set of fields with possibly different types. - -## Custom Types - -User is allowed to create custom types based on base-types. - ---- - -Further section needs some work. This is WIP document. diff --git a/docs/directives.md b/docs/directives.md new file mode 100644 index 00000000..a78fc11e --- /dev/null +++ b/docs/directives.md @@ -0,0 +1,15 @@ +# Directives + +Compiler directives are special instructions for the compiler, not intended for daily use but important for understanding language features. + +## `#extern` + +Tells compiler a component lacks source code implementation and requires a runtime function call. + +## `#bind` + +Instructs compiler to insert a given message into a runtime function call for nodes with `extern` components. + +## `#autoports` + +Derives component inports from its type-argument structure fields, rather than defining them in source code. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..cc233b92 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,124 @@ +# FAQ + +This FAQ addresses questions not covered in other documentation pages. It serves as a reference for the reasoning behind certain language design decisions. + +## Is Neva "classical FBP"? + +No, but it many ideas from Flow-Based Programming (FBP). You can read more on [FBP page](./fbp.md) + +## Why are array-ports needed? + +Array-ports are necessary for combining data from multiple sources. + +## Why can't components read from their own array-inports by index? + +Allowing this would lead to potential blocking or crashes if the parent node doesn't use all slots. Enforcing exact slot usage would negate the flexibility of array-ports. + +## Why can components read from sub-node's array-outports by index? + +This is necessary for critical cases like "routing" where data needs to be sent to specific handlers based on certain conditions. While it could be implemented with if-else chaining, that approach would be tedious and less efficient. + +## Why is outport usage optional while inport usage is required? + +In general it's impossible to tell if component is able to produce result without all inports being used. Inports are requirements to trigger computation. Outports, on the other hand, are results you can use anyway you want. This design allows for discarding unwanted data. + +## Why are there no int32, float32, etc.? + +Neva opts for simplicity, providing only int and float types as a compromise between flexibility and ease of use. This way you may have much less type-convertsion in your code. + +## Why have integers and floats instead of just numbers? + +Separate int and float types provide: + +1. Better handling of large numbers and overflow issues +2. Improved performance for integer operations +3. More predictable comparisons +4. Enhanced type safety + +## What determines which entities are in the builtin package? + +Entities in the builtin package are: + +1. Frequently used +2. Used internally by the compiler + +## Why Emitter implemented like an infinite loop? + +Const nodes are implemented like infinite loops that constantly sends messags across their receivers. This covers all the usecases but also requires locks because we usually want control when we send constant messages. + +Alternative to that design would be "trigger" semantics where we have some kind of `sig` inport for const nodes. Instead of starting at the program startup such trigger component would wait for external signal and then do the usual const stuff (infinite loop with sending messages). + +**The problem #1 with this approach - we still needs locks**. It's not enough to trigger infinite loop. E.g. in "hello world" example nothing would stop `msg` const node to send more than 1 `hello world` message to `print`. + +**Possible solution for that would be to change semantics and remove infinite loop logic**. Make const node send signal only after we trigger it via sig port. The problem with this approach is that there is many usecases where we want infinite loop behavior. Think of initial inports - e.g. `requestSender` component with `data` and `url` inports where `data` is dynamic and `url` is static. It's not enough to send static url value just once (`requestSender` must remember it, we don't go that way because that leads to bad design where components know where they get data from - this is huge violation of transport vs logic separation). + +This problem by itself is fixable with using external sources like signals. When we have some static inport we usually have some kind of dynamic data that must be used in combination with it. Even though it would lead to making networks more complicated (locks do this too though), it's possible solution. But we have another problem: + +**Problem #2** - `$` syntax sugar. + +Another problem with previous solution (const nodes have sig inport and they send one message per one signal) is how use `$` syntax sugar. + +Currently it's possible to _refer constants_ in network like this:`$msg -> ...` + +This won't be the thing because we have to have not just entity reference but regular ports like `$msg.sig` and `$msg.v`. This is not a disaster but rather strange API. + +Where this `$msg` node come from? Is it clear to me as a user that there are implicit nodes $ prefix for every constant that I can refer? Why these `sig` and `v` inports? Because this is how `std/builtin.Const` works? Why do I have to know this? Why do I have to know how syntax sugar is implemented under the hood in compiler? + +Finally another possible solution to that could be `Trigger` components in combination with `Const` components. The difference would be that const behaves like infinite loops that requires locks and triggers behaves like single sending triggers (no lock required). + +Problems with this solution are: + +1. Now we have 2 ways to do the same thing. Do I need to use const? Or trigger? When to use what? +2. Trigger either must be used in combination with `#runtime_func_msg` directive (this violates principle that user must be able to program without directvies) or there must be sugar for triggers. + +It's possible in theory to create sugar for triggers but the language could be too complicated with so many syntax features. This conclusion by itself is questionable but in combination with the first problem - having 2 ways to use static values. Looks like it's better not to have triggers. + +All this leads to a conclusion that the only good semantic for constants is the current ones - infinite loops that requires locks. The need to have locks is not a fancy thing by itself, but "then connections" sugar made this pretty simple. + +## Why have streams builtin? + + + +Sub-streams solve the problem of iterating over collections in FBP, where we lack mutable state and code-as-data. They provide a way to know when a list ends, which is crucial for implementing patterns like `map`. + +## Why are Neva's streams different from classical FBP? + +Neva supports infinite nesting for streams because they are implemented with just structs. However, nested streams are not used to represent structured data because there are structs, lists and dictionaries. + +## How to work with components that expect `T` when you have `stream`? + +It depends on what you want to do with it. Generally you're ok with `Map/Filter/Reduce` for data transformations and `For` for side-effects. If you have more complex case you can access `.data` on stream item directly. + +## Why isn't `Main:stop` has `int` type? + +Using `any` allows for more flexible exit conditions. Interpreting any `int` as an exit code could lead to unintended behavior, especially with union types or any outports. + +## Why use structural subtyping? + +1. Reduces code, especially for mappings between records, vectors, and dictionaries +2. Nominal subtyping doesn't prevent mistakes like passing wrong values to type-casts anyway + +## Why have `any`? + +Neva's `any` similar to Go's `any` or TypeScript's `unknown`. Is necessary for certain critical cases where the alternative would be an overly complicated type system. + +## Why can only primitive messages be used as "literal network senders"? + +1. Easier type inference for the compiler +2. Keeps networks readable (complex literals would be hard to read) + +## Why is there no syntax sugar for `list` or `maybe`? + +Consistency with other type syntax and avoiding confusion with different syntaxes in other languages (should it be `[]T` or `[]T`? Or `[T]`? Etc.) + +## Why is there inconsistent naming in stdlib? + +Some basic components (e.g., Len, Eq) follow naming conventions from other languages for familiarity. + +## What's the reasoning behind Neva's naming conventions? + +Names are chosen to be familiar to most programmers, easing the paradigm shift. + +## Why `struct` and `dict` literals require `:` and `,` while struct declarations don't? + +This makes literals similar to JSON and consistent with languages like Python, JavaScript, and Go. It also distinguishes between `type` and `const` expressions. diff --git a/docs/fbp.md b/docs/fbp.md deleted file mode 100644 index 7e898375..00000000 --- a/docs/fbp.md +++ /dev/null @@ -1,57 +0,0 @@ -# Flow Based Programming - -Nevalang's dataflow is not pure FBP but FBP influenced Nevalang the most. This document describes differences between Nevalang's and FBP's dataflow. - -## Granularity - -This might be the most important difference between FBP and Nevalang. FBP is intended for high-level orchestration of lower-level components, written in controlflow languages, while Nevalang is truly general purpose and expects you to write your whole program in Dataflow. For instance, in FBP you are not expected to write math logic, but instead to do it in controlflow language and then use FBP as a glue. In Nevalang, on the other hand, you have math components and you are expected to use them instead. - -## Controlflow - -Granularity difference leads to differences in how we threat controlflow in our dataflow environment. - -FBP has an idea that there are atomic and complementary components. Atomic components are implemented in the "host" platform (e.g. Java) while complementary ones are dataflow. - -Nevalang also has atomic/complementary (it's called low-level/native and high-level/custom/normal). However, the difference is that Nevalang provides you with ready to use low level components (written in Go) and you never supposed to write controlflow by yourself. This is exactly the way operator and builtin functions works in controlflow languages. E.g. in Go some functions are implemented in assembler. - -The only time when you are intended to write controlflow in Nevalang is when you work on stdlib (which means you are a language contributor) or you write some glue code to integrate Go code into Nevalang or vice versa. Anyway, "normal" way of doing things in Neva is to stay in pure dataflow land as long as possible. - -## Garbage Collection, Ownership and Immutability - -FBP is not garbage collected and has concept of ownership. Each message has one "process" owner. Message should be either copied or ownership must be passed. Thanks to this data mutations are possible. However, this has some downsides like not being able to implement fan-out without using some special components, because FBP tries to avoid mutation and performance (copying) problems. - -Nevalang is garbage collected, there is no concept of ownership/borrowing and all data is always immutable. This has performance downside but on the other hand you don't have to think about ownership or mutation problems. E.g. data race is not possible in Nevalang (don't be confused with race condition which is possible in both Nevalang and pure FBP). - -Mutations are possible with using special unsafe package from stdlib but should be considered as unwanted optimization tricks when it's clear that nothing else can't help. However, even this way component can only mutate messages inside its own network, not child or parent, so there's some visibility scope protection. Another way to say that Nevalang allows data mutation is to say that it's implemented in controlflow langauge. That's true from implementation point, but not end-user experience. Exact same argument applies for the controlflow interop. - -## Nodes Shutdown and Restart - -FBP and Nevalang both has concept of nodes, but in FBP it's called "process". The similarity is that in both cases we are talking about components instances. However, FBP's process has concept of "state" which means process could be started, suspended, restarted or shutted down. There are some technics that allow manipulate this behaviour by enabling/disabling some specific parts of the network. - -On the other hand, Nevalang's nodes always run. After the program started, all the nodes there are started. Node is not doing any work until there's some condition met e.g. message received to some port, but this happens automatically without user's intention. In other words, Nevalang's node is always started, suspended, restarted and disabled automatically when needed. - -## Static Typing - -FBP doesn't have concept of static typing except coding atomic components in statically typed controlflow. Dataflow part of the FBP is dynamically typed like JavaScript. - -Obviously, you have to write more runtime validations and tests. Also IDE support will be worse. Because of that Nevalang comes with static type system. - -Nevalang's type system is relatively powerful, e.g. it supports generics and structural sub-typing. However, it's also relatively simple, which means you shouldn't spend a lot of time working with types, but also some things cannot be expressed - it may be "unsound" for something who came from something like static typed FP. - -## Similarities - -Both dataflow, both support implicit parallelism. Nevalang's dataflow looks more like FBP than something else (given the differences we discussed). Also a lot of terminology is shared. - -## Terminology - -**FBP / Nevalang** - -- Component / Component - - Atomic / Native - - Complementary / Normal -- Process / Node -- Connection / Connection -- Ports / Ports -- IP / Message -- IIP / Constant -- IP Tree / Structure or Dictionary diff --git a/docs/formatting.md b/docs/formatting.md deleted file mode 100644 index f3fbb352..00000000 --- a/docs/formatting.md +++ /dev/null @@ -1,30 +0,0 @@ -# General - -Don't try to be as short as possible (and don't try to be as verbose as possible). Seek balance instead. Readability is king. Code that is understandable at a glance is good code. There are multiple ways to write the same thing, sometimes shorter, sometimes longer. You should choose the most appropriate form for each specific usecase. - -# Nodes - -write nodes in one line if - -- < 80 chars -- no aliases - -good - -```neva -flow Main(start) (stop) { - Foo, Bar, Baz, Bax - --- - :start -> foo -> bar -> baz -> bax -> :stop -} -``` - -bad - -```neva -flow Main(start) (stop) { - f Foo, Bar, Baz, Bax - --- - :start -> foo -> bar -> baz -> bax -> :stop -} -``` diff --git a/docs/interface_entity.md b/docs/interfaces.md similarity index 97% rename from docs/interface_entity.md rename to docs/interfaces.md index 0c849bc5..f72f9df7 100644 --- a/docs/interface_entity.md +++ b/docs/interfaces.md @@ -1,4 +1,4 @@ -# Interface Entity +# Interfaces Interface is a _component signature_ that describes _abstract component_ - its input and output _ports_ and optional type parameters. Interfaces are used for _dependency injection_. diff --git a/docs/motivation.md b/docs/motivation.md deleted file mode 100644 index 5746bcdc..00000000 --- a/docs/motivation.md +++ /dev/null @@ -1,61 +0,0 @@ -# Motivation - -There are thousands of programming languages, why need another one? This document describes the motivation behind Nevalang in detail. - -Before we dig into the details, here's super high-level idea: it's possible to create a better language, where programs are easier to understand, debug and create. Unique set of features properly combined together could lead to such language. Nevalang is nothing but an attempt to create such language. - -## General Purpose Dataflow - -Almost all general purpose languages are controlflow. Dataflow languages on the other hand are usually domain specific. There are general purpose dataflow but very few of them and they are either not pure (you have to write controlflow) or not "good enough" (i.e. only works with JavaScript or has no static types). - -## General Purpose Visual Programming - -It's a shame that we don't have at least one visual programming language in out TOP-10. Imagine having something as good as Java or Python but visual. - -Turns out dataflow and visual programming are connected. Controlflow is hard to visualize so we need good general purpose dataflow for strong visual programming. - -## Text/Visual Hybrid - -One of the reasons visual programming haven't succeeded is the fact that we've created a ecosystem that bound to text. E.g. version control systems like Git are text-based. Also, you need to be able to do code review. - -Some visual languages store code in JSON, Yaml or XML. However, this is not easy to read and write. Syntax is important, and that's why Nevalang has it's own minimalistic C-like syntax. Visual representation fully reflect this syntax. - -## Implicit Parallelism - -There are not many implicitly parallel languages - languages where you don't have to (or even more than that, you are not allowed) to think in terms of threads, locks, channels, etc. You "just write your code" and "system" somehow automatically figures out how to insert parallelism where possible. - -Dataflow is the perfect model for that. You just have nodes and they, because there's no shared state, can operate completely parallel. And this is exactly how Nevalang works. - -Imagine a pipeline of 3 nodes. There's input data that goes to node A, then from A to B and finally from B to C. Then imagine infinite stream of data going to A. In Nevalang all A, B and C can work at the same time. This is an example of implicit parallelism without explicit parallel connections in network topology. And of course you can have literally (and visually) parallel connections to nodes that process data in parallel. You can use technics like round-robin to handle highload at the level of your executable binary. - -## Better Language - -It's hard to explain the core motivation without telling that I always felt a need for a better language. How great would it be to collect all the best working technics into one perfect language? Besides all the listed features, I had a very specific opinion on topics such as type-system, error handling, syntax, etc. In Nevalang I tried to do my best. - -Core motivation for creating Nevalang is belief that it's possible to create a more optimized tool for our daily tasks. Do more with less effort. There were always questions like "what is the best way to do X?". All the things like dataflow basically consequence from that. - -### Type-System - -It's very hard to find a perfect balance for a type-system (and semantic analysis in general) so you don't "fight" it, but compiler helps you instead. In my personal opinion Go is very close to that, but it has too much data-types and it uses nominative sub-typing for them. On the other hand TypeScript uses structure sub-typing but it has `any`. Go also has but in Go any is a type you can't do anything with. In TS you _can_ do anything with any. Also in TS you usually interop with unsound JS ecosystem. Also TS is too complex, its type-system literally turing complete. - -Nevalang tries to find balance between complexity and reliability. Its type-system looks like something between TypeScript and Go, probably closer to Go, but with just a little bit more strictness. It inherits structural sub-typing for data-types from TypeScript though. Sub-typing for interfaces is structural just like in Go. It also supports generics with constraints. - -### Error Handling - -Nevalang follows Go's "errors are values" mantra. In Nevalang error is just a data-type for a message. A structure (of a very simple shape), to be more specific. Just like Go's functions can return several values and one of them (last usually) can have type `error` (and the variable that store this value usually called `err`), Nevalang's components can have `err error` outport. In Go (and any controlflow) you check error (using `if` or `try-catch` sequence) and either handle or throw/return. And Nevalang you _send_ error somewhere. It could be panic component, logger, or maybe your own `error` outport. This is analog for throwing. Maybe you want to add some context before that, that's also easily possible. - -Rust's approach to errors is even better than Go's. It has `Result` type that's either error or result. This is not very much different than having several return values but Rust also have special operator that allows you to "ignore" error-handling while still handling error. That's a syntax sugar that implicitly inserts error-handling into your code everywhere you use this special operator. Nevalang also have this, it's called `?` operator. - -Finally, Go _does_ allow you to ignore errors. In Nevalang you always have to use `error` outport of your sub-nodes (and your own) if it's present. - -### Tracing - -Each message has its own path that is always updated whenever it moves from one place to another. This is something like a stacktrace that you get with exception in languages with exceptions like Python or JavaScript, but not just for exceptional situations, but for literally every message in the program. - -Go doesn't have stacktraces for errors (only for `panic`) and that leads to abuse of `fmt.Errorf` with obscure directives like `%w` that you have to memorize. It's good that nowadays LLMs can more or less automate that but it's not perfect. In Nevalang you don't have to do anything to get a full trace of the message, from the place it was created up to the point we log the trace. - -It makes error handling even simpler, most of the time you should be ok with nothing but just `?`. However, it's not only for error handling. You can track _any_ message this way. Any! Imagine what debugging possibilities in opens. - -### Next-Gen Debugger - -Dataflow + tracing opens the door for next-generation debugging tool where you can visually set breakpoints on a specific connections in your network graph and stop the program when message arrives there. You can then observe the message or even update its value. diff --git a/docs/network.md b/docs/networks.md similarity index 99% rename from docs/network.md rename to docs/networks.md index 64ae1d72..d4e5f65f 100644 --- a/docs/network.md +++ b/docs/networks.md @@ -1,4 +1,4 @@ -# Network +# Networks Connections forms component's network. There are array-bypass connections and normal connections. Array bypass are very simple, normal takes many different forms. Connections are also have recursive hierarchy and can be mixed in a lot of forms. diff --git a/docs/paradigm.md b/docs/paradigm.md deleted file mode 100644 index 5128dfa4..00000000 --- a/docs/paradigm.md +++ /dev/null @@ -1,59 +0,0 @@ -Here we focus on it's paradigm - Dataflow. - -## Dataflow - -There are 2 high level programming paradigms: Dataflow and Controlflow. Paradigms like OOP or FP are specific variations of Controlflow while Actors or CSP are specific variations of Dataflow. - -Dataflow programming is paradigm, that explains computation in terms of directed graphs. It takes many forms: OOP (by Alan Kay), CSP, Actor-model, FBP, etc. FBP is the one that influenced Nevalang the most. However, Nevalang is not original FBP in many ways. FBP, for instance, is not pure dataflow because you intended to write atomic components, which means using controlflow. - -Common things for dataflow are nodes, connections and some kind of programmed flow for (probably async) message passing. There are popular general purpose controlflow languages that support some dataflow subset: Go's Goroutines and Channels and Erlang's Actors. Problem with these languages is that controlflow and dataflow are very different and combine them is harder than to combine different controlflow paradigms (e.g. procedural+functional). E.g. concept of variables is very foreign to dataflow. - -On the other hand there are (more or less pure) dataflow languages that are not general purpose: Unreal Blueprint, Labview, etc. Some of them are pure, some of them are not. What about general purpose pure dataflow? - -### Dataflow in Nevalang - -Nevalang takes a next step in dataflow by making it the main paradigm. Controlflow is supported but is far from "first class citizen". All the native API is pure dataflow i.e. expressed in terms of pure dataflow abstractions. - -There are low-level components that are could be implemented in controlflow under the hood, but the API exposed for Nevalang programmer is always pure dataflow. - -Nevalang has such dataflow concepts as: - -- Components and Nodes (instances) -- Ports (input and output) and (buffered) Connections between -- (Immutable) Messages - -### Code is Code, Data is Data - -In many controlflow langauges code and data exist in a single "space" of memory so we can pass functions around. However, there's no such thing in Nevalang. What is passed between components are messages, not other components. - -### Static Dependency Injection - -In Nevalang you can write polymorphic code by creating interfaces and using them as dependencies. Parent component will have to provide implementation. This is something very common for controlflow programmers who came from statically typed languages. But because _code is not data_ you can't do it dynamically. You can't "pass component" as a message. So all DI happens statically. Good news is that this is usually enough. - -## Controlflow - -Controlflow is the idea that there is some execution thread that follows some instructions and mutates some external state. It has many faces: from assembler to Haskell. - -Why e.g. functional programming (pure/impure) is a controlflow? Core idea is the same, there'is a "thread" executes something. Either it's binary instructions or evaluation of a callstack - thread is "moving" through the program. - -In Dataflow it's vice versa - threads are just "there". What is moving is data. E.g. Go's CSP is pure dataflow (without the rest of the language). - -## Imperative vs Declarative - -Not to be confused with imperative/declarative dilemma. Nevalang is declarative dataflow language, Haskell is declarative controlflow, C is controlflow imperative. - -Imperative answers the question "how?", declarative "what?". E.g. Go's loop is expressed in terms of explicit controlflow instructions like `break`, `continue`, etc. Nevalang comes abstractions for message passing. - -On the other hand Haskell wants you to define three of expressions that is somehow evaluated to a value. However, Haskell has a concept of "call/return", that are clearly controlflow instructions. - -Imperative Dataflow is probably possible but dataflow is usually declarative. It's hard to say why, maybe the fact that computation is expressed in terms of graphs has something to do with this. - -## Controlflow vs Dataflow - -Now when we know what are these, let's learn what are their upsides and downsides. - -Dataflow is easy to visualize. It's also good for parallelism. Obviously, good for message passing. - -It's hard to enforce order though - things like "A can happen before B but it must happen only after". In controlflow this is not even a question: `a(); b()` - in most controlflow guaranteed "A then _always_ B". - -Obviously controlflow has benefit from being dominated platform for thinking for a lot of time. Of course you will easily find lots and lots of materials, libraries, articles, videos, etc. Not so much for dataflow. However, all it means is only that we have to research and implement good dataflow. diff --git a/docs/program_structure.md b/docs/program_structure.md index 961f53da..cd0c6da1 100644 --- a/docs/program_structure.md +++ b/docs/program_structure.md @@ -1,71 +1,36 @@ # Program Structure -```yaml -build: - modules: - manifest: - language_version: string - - packages: - files: - imports: {} - entities: {} -``` - ## Build -Build is the most high level abstraction and it's the first compiler stage. Building (the program) means collecting all the source code together in one place, so it's possible to work with it as with single thing. - -Compiler downloads (those that are not cached) all the needed _dependencies_ and bundle them together into a object, that it can analyze. It is known at compilation time which _module_ in the build is the _main module_. +Build is the highest-level abstraction and the first compiler stage. It collects all source code, including dependencies, into a single object for analysis. ## Module -Module is a set of _packages_ on a file-system that have single root directory with `neva.yml` file that called _manifest_ or _manifesto_. Manifest defines: minimal language version it supports and all the _3rd-party_ modules it depends on. Each dependency has _path_ and _version_. When compiler _builds_ the program it recursively downloads each module with all its dependencies. - -### Main Module - -Main module is the one that contains _main package_ with _main component_. In other words it's the entry point for compiler to generate a program. +A set of packages with a root directory containing `neva.y(a)ml` (manifest). The manifest defines the minimum supported language version and dependencies. The main module contains the main package with the main component. ## Package -Package consist of one or more files. Files don't affect the visibility scope of the entities. Each entity defined in one file is visible from another file in the same package. In terms of the file-system package is set of files in a single directory. Each package has unique path where module (project) is the root. It doesn't matter how you include packages into eachother, all it affects is names of the packages. You can, however, (and should) to express your architecture with your directory structure. It is especially important because your directory structure dictates your namespaces. - -### Main Package - -Main package is the one that can serve as an entry-point. It must contain _component_ `Main` with special signature and no exported entities. As any other package it can consist of any amount of files. It's ok for any module in a Build to contain main package, but only one main package is actually used as an entry point. +A set of files in a single directory. Entities in one file are visible from other files in the same package. The main package must contain the `Main` component with a special signature and no exported entities. ## File -File with `.neva` extension that contains entities and is a part of the package. File consist of imports and entities. Imports are special instructions that allow to depend on other entities from other packages. All imports are explicit except imports form `builtin` package which is imported implicitly to each file at desugaring stage. +A `.neva` file containing entities and imports. Imports allow dependencies on entities from other packages. The `builtin` package is implicitly imported. ## Imports -### Path and Alias - -Import statement consist of _path_ and optional _alias_. Alias means how we refer that package in source code. E.g. `import { foo/bar }` imports `foo/bar` package and assigns `bar` alias to it. That is, by default last part of the path equals alias. If you want custom alias, which is only allowed if you have imports with duplicated aliases like `foo/bar, baz/bar`. - -There are 3 types of imports: _std_, _3rd-party_ and _local_. Std looks like `strings` or `net/http` - just pkg name. 3d-party looks like `/` where `mod_name` is the name defined in the module's manifest file `neva.yml`. Finally local imports are of the form `@/` where `@` is the module's root. - -### Entity Reference Resolution - -When you reference some entity by its name and (optional) package modifier (`Foo` is reference without package modifier and `foo.Bar` is with package modifier), Nevalang's compiler _resolves_ that _entity reference_. - -If you refer without modifier like `Foo` then it first looks for Foo entity inside the package - this or other files in that directory. - -**Each file contains implicit import** of `builtin`. -Note that so if file is not found in local package, compiler tries to resolve it like `builtin.Foo`. This is how compiler resolves builtin references without package modifier like `int`. +Import statements consist of a path and optional alias. There are three types: std, 3rd-party, and local. Entity references are resolved first within the package, then in `builtin`. ## Entity -Entity is very important abstraction and the most important entity is _Component_. Nevalang is 100% declarative so all you actually do is defining entities and their relations. Entities is what forms a Nevalang package. Nevalang program is essentially a bunch of entities. **Each entity has unique name across package namespace**. - -There are 4 _kinds_ of entities: +The core abstraction in Nevalang. There are four kinds: 1. Types - message shape definition 2. Constants - reusable messages with static values 3. Interfaces - abstract components 4. Components - computation units -### Visibility Scope +Entities can be public (`pub`) or private, determining their visibility outside the package. + +## Entity Reference -Entities could be public or private what means - can they be used (imported) outside of the package or not. Keyword `pub` is used to denote that. `Foo` is private entity and `pub Foo` is public entity. +Entity references include an optional package name and the entity name. Package names can be omitted for entities in the same package or in `std/builtin`. Local entities with the same name as builtin entities shadow them. diff --git a/docs/style_guide.md b/docs/style_guide.md new file mode 100644 index 00000000..e7a6f2b9 --- /dev/null +++ b/docs/style_guide.md @@ -0,0 +1,34 @@ +# Style Guide + +This guide sets standards for Nevalang code organization, formatting, and naming conventions to ensure consistency and readability. + +## Code Organization + +- **File Length**: Aim for files under 300 lines. Longer files should be split. + +## Formatting + +- **Line Length**: Keep lines under 80 characters. +- **File Size**: Aim for files under 100 lines. +- **Comments**: Avoid comments. Use clear naming instead. If necessary, keep them short and simple. +- **Indentation**: Use tabs over spaces. + +## API Design + +- **Generics**: Use when data type consistency across ports is important. +- **Components**: Use outports for different data paths, structs for related data. +- **Network**: Prefer simple topologies (pipes, trees) over complex networks. +- **Ports**: Limit to 3 inports and 5 outports max. +- **Interfaces**: Keep small with minimal ports. + +## Naming Conventions + +Names should inherit context from parent scope. Good naming eliminates need for comments. + +- **Packages/Files**: `lower_snake_case`, up to 3 words. +- **Entities**: `CamelCase` for types, interfaces, components. `lowerCamelCase` for constants. +- **Interfaces**: `CamelCase` with `I` prefix. +- **Components**: Noun, like for functions in most languages. +- **Ports**: Short `lowerCamelCase`, up to 5 letters. +- **Nodes**: `lowerCamelCase`, distinguish instances by meaning. +- **Enums**: Singular form. diff --git a/docs/type_entity.md b/docs/types.md similarity index 58% rename from docs/type_entity.md rename to docs/types.md index 3621c624..ee985f4b 100644 --- a/docs/type_entity.md +++ b/docs/types.md @@ -1,4 +1,4 @@ -# Type Entity +# Types Type entity (type definition) consist of an optional list of _type parameters_ followed by optional _type expression_ that is called _body_. @@ -6,6 +6,66 @@ Type entity (type definition) consist of an optional list of _type parameters_ f Type definition without body means _base_ type. Compiler is aware of base types and will throw error if non-base type has no body. Base types are only allowed inside `std/builtin` package. Some base types can be used inside _recursive type definitions_. +- _any_ +- _maybe_ +- _bool_ +- _int_ +- _float_ +- _string_ +- _dict_ +- _list_ +- _enum_ +- _union_ +- _struct_ + +### Any (Top-Type) + +`any` is a _top-type_. It means that any other type is a _sub-type_ of any. That means you can pass any type everywhere `any` is expected. However, since `any` doesn't tell anything about the type, you cannot pass message of type `any` anywhere where more concrete type is expected. You need to either rewrite your code without using any or explicitly cast `any` to concrete type. + +### Maybe + +Maybe represents value that maybe do not exist. One must unwrap maybe before using the actual value. + +### Boolean + +Boolean type has only two possible values `true` and `false` + +### Integer + +Integer is 64 bit integer number + +### Float + +Integer is 64 bit floating point number + +### Strings + +Strings are immutable utf encoded byte arrays + +### Maps + +Maps are unordered key-value pairs with dynamic set of keys. All values must have the same type + +### List + +List is a dynamic array that grows as needed. All values in the list must have the same type. + +### Enums + +Enums are set fixed set of values (members) each with its own name. They are represented in memory like integer numbers. + +### Union + +Union is a _sum type_. It defines set of possible types. + +### Struct + +Structures are product types (records) - compile-time known set of fields with possibly different types. + +## Custom Type + +User is allowed to create custom types based on base-types. + ## Recursive Type Definition If type refers to itself inside its own definition, then it's recursive definition. Example: `type l list`. In this case `list` must be base type that supports recursive definitions. Compiler knows which types supports recursion and which do not. @@ -39,3 +99,4 @@ Such expression consist of _entity reference_ (that must refer to existing type Type expressions that cannot be described in a instantiation form. +