Franca programming language specification

Published Jul 14, 2018. 27 minutes to read.

This is a living document.

Franca is a programming language, designed as part of a personal research project in programming language and compiler design.

I might create a compiler for it some day, but there is no immediate goal to do so. For now, the language is purely theoretical.

This specification assumes readers have general working knowledge in computer science.

Here be dragons.

What is Franca?

Franca is general purpose, object-oriented, statically typed, imperative, compiled programming language. It is syntactically similar to Python and inherits a number of concepts from other programming languages, such as C, Java, C#, etc.

The language is designed with emphasis on program correctness and is therefore very opinionated about certain aspects related to the structure of a program.

Major changes

  • 05.06.2021 - Language revision (too many changes to list)
  • 01.07.2019 - Dependency injection
  • 01.04.2019 - Syntax update (guards, conditionals)
  • 24.03.2019 - Syntax update
  • 03.10.2018 - Transport types.
  • 12.09.2018 - Events and visibility updates.
  • 14.07.2018 - Initial version.

TODO

  • Lazy generators.
  • Default types, imports, etc.
  • Streams and stream processing support in language level. (stream keyword)
  • Cast syntax specification (x as Type).
  • Annotations management in runtime (annotation reflections, collectors, DIC).
  • Documentation comments.
  • Compile time app configuration and properties.
  • Compile time reflections (query types in runtime - all instances of X type, all annotated with, etc).
  • Serialization and deserialization, object versions.
  • Explicit override for methods overriding imported type.
  • Easier way to define complex data structures. Sort of typed inline JSON or Hash/Dict (extended transport types).
  • noop to indicate empty method blocks and similar.

Hello World

package app

external type Main:
    external main:
        println('Hello world')

Kitchen sink

Small sample of the look and feel of the language.

package app

@Annotation
public type FooBar:
    extends BaseApplication
    extends ApplicationService
    implements Executable
    implements ServerApplication

    cast ContainerApplication:
        new ContainerApplicationWrapper(this)

    overload ApplicationCollection append:
        -> ServerApplication other # Argument to append overload

        ApplicationCollection.of(this, other)

    event ApplicationStartEvent startEvent

    constructor:
        public -> Uint16 port # Argument with field escalation
        public -> InetHost host # Argument with field escalation
        !IllegalArgumentException # Checked exception - must be handled or forwarded by caller

        throw new IllegalArgumentException('Illegal port') if port < 1025
        throw new IllegalArgumentException('Illegal host') if host.isEmpty()

    public start:
        super.start()
        emit new ApplicationStartEvent() to startEvent

Language specification and features

Operators

Arithmetic: +, -, /, *, %, ++, --

Comparison: ==, ===, >, <, >=, <=

Bitwise: &, |, ^, ~, <<, >>, >>>

Logical: &&, ||, !

Assignment: =, +=, -=, /=, *=, %=, ++=, --=, <<=, >>=, &=, ^=, |=

Ternary: ? and :

Packages

Packages are the namespaces used to organize code files and types into logical units of code within your assembly.

Type modifiers

abstract - type can not be instantiated, only extended. Implicitly open. open - type that can be optionally subclassed. Franca types can not be subclassed by default.

Visibility modifiers

In Franca, all visibility is structured in two main umbrella structures - local and foreign assembly.

Local assembly is defined as current target of compilation for this source tree - for example, binary, library etc.

Foreign assembly is defined as all assemblies that does not belong to current source tree - think, vendor libraries.

private

Private types and members are only visible to their nearest construct.

Types marked private are only accessible within the same exact package. For example, type Foo defined in package app.bar.baz is only visible to other types in app.bar.baz package.

Type members marked private are only accessible within that type and not in subtypes.

All type members are by default, private.

protected

Protected members are only accessible within given type and any subtypes it can have.

Protected members can only be used within types with modifier open and abstract. Using protected in regular types would result in compile time error.

Types can not have protected visibility - only type members can.

public

Public types and members are accessible within local assembly. All types are by default, public.

external

External types and members are accessible from current and foreign assemblies.

Default visibility

  • Types are public by default.
  • Type members are private by default.
  • Type constructors, overloads and casts inherit visibility of the type by default.
  • Enum values inherit the visibility of the enum definition by default, except for members - members are private by default.

Primitive types

TypeDescription
VoidNull label, used exclusively for type hinting in generics.
StringA string type.
CharSingle logical unit of String, usually a character or symbol
BoolUsed to declare variables to store the Bool Values, true and false
SByte8-bit signed integer. Value range from -128 to 127 (inclusive)
Byte8-bit unsigned integer. Value range from 0 to 255 (inclusive)
Int1616-bit signed integer. Value range from -32,768 to 32,767 (inclusive)
UInt1616-bit unsigned integer. Value range from 0 to 65,535 (inclusive)
Int3232-bit signed integer. Value range from -2,147,483,648 to 2,147,483,647 (inclusive)
UInt3232-bit unsigned integer. Value range from 0 to 4,294,967,295 (inclusive)
Int6464-bit signed integer. Value range from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
UInt6464-bit unsigned integer. Value range from 0 to 18,446,744,073,709,551,615 (inclusive)
Int128128-bit signed integer. Value range from −(2^127) to 2^127 - 1
UInt128128-bit unsigned integer. Value range from 0 to 2^128 − 1
Decimal3232-bit signed decimal number.
Decimal6464-bit signed decimal number.
Decimal128128-bit signed decimal number.
Float3232-bit signed binary floating point number.
Float6464-bit signed binary floating point number.
Float128128-bit signed binary floating point number.

Primitive literals

LiteralDescription
nullNull value, nothing. Can be used as value only, for type hinting use Void.
trueTruthy bool value.
falseFalsy bool value.

Labels and fields

Labels and fields in Franca language are effectively named pointers that hold a reference to the object instance that was assigned to given label or field in current scope, similar to class properties and variables in other object-oriented programming languages.

Once a label or field is defined it will always and forever refer to the same object, unless marked mutable, and the reference is changed later in the code.

If you require a mutable label - or a variable, you must prefix the definition of the label with keyword mut.

The same rules apply to the type fields (properties) of Franca types. Fields in types are also immutable by default, unless explicitly defined mutable.

For example:

# ...
String foo = 'bar' # Immutable label
mut String baz = 'biz' # Mutable label
# ...

Labels and label definition

Basic syntax <Type> <identifier> = <value>

Mutable label mut <Type> <identifier> = <value>

Lazy labels

All labels can have lazy initializers. Lazy labels are computed on first-real-read. Passing labels around as references does NOT trigger value computation, nor does placing the values in structs and arrays.

Lazy values can be both immutable and mutable. Lazy values can be optionals.

Lazy values are computed synchronously and are scoped to the object defining the computation - this will always point to the object type where lazy variable is defined.

<Type> identifier = lazy:
    <value>

mut <Type> identifier = lazy:
    <value>

Field getters and setters

Getters and setters allow for inline explicit definition of field mutators - get and set.

get MUST return value of type matching the field.

set accepts a single virtual value referred to using it label, and is of same type as the field. set returns Void.

type MyObject:
  extends FooBaz
  implements BazBar

  mut String fooBar =
    get:
      'moo'
    set: # Requires field to be mutable.
      print(it)

  mut String fooBar =
    get:
      'moo'
    set:
      print(it)

No global functions or labels

There are no global functions and no global labels. All functions are members of objects - methods, all labels are either fields of objects or local scope labels.

All labels are objects

Everything in Franca is object - including primitives, type definitions etc.

One source file, one root construct

One source file can only hold one instance of whatever construct that source file describes - type, interface, partial and others.

Simply put, you can not have multiple types defined in the same source file, even if they have different visibility, as you can do, for example, in Java.

No nested structures

Types and other root structures can only be defined at the root level. You can not nest type definitions, interfaces etc.

(Almost) no inline implementations

Inline implementations of interfaces are not permitted, except if they are implementations of functional interface or Function<?>, effectively - lambdas.

Implicit and explicit return

For methods or functional implementations where return type is not Void, the last expression of the code block is considered to be return statement. You do not need to prefix the expression with return keyword in such scenarios.

Within conditionals or if functional block has multiple return points, you can prefix an expression with return keyword to indicate a return point.

Last expression of a functional block can also be throw expression, in which case return is not required.

Nulls and null safety

As a general rule, no label can be assigned null value unless explicitly marked nullable by suffixing the type in label or field definition with a question mark, or by wrapping a value with Nullable type by hand using Nullable.of(...) static method.

The question mark suffix is just a syntax sugar with some magic. Eventually, it still translates to Nullable<T> and both can be used interchangeably, although using question mark suffix is recommended.

Nullable type exposes methods <T> get, <Bool> absent, <Bool> present, and a few other utility methods that should ease up working with nullables.

Nullables are the only constructs capable of having null value. Attempt to assign null to non-nullable label in code will result in compile error.

Attempt to assign null to non-nullable label in runtime will result in runtime exception - this can happen when, for example, using foreign native interfaces to receive foreign inputs.

For example:

String notNull = null # Compile fail

# ...
String? nullalbe = null # Compile success

# ...
String? nullable
nullable = null # Compile success

#...
Nullable<String> explicit = null # Compile success

#...
Nullable<String> explicit = 'Foo Bar' # Compile success (label auto-wrapped)

#...
Nullable<String> explicit = Nullable.of('Foo Bar') # Compile success (label wrapped explicitly)

Label and field assignment and definition

Label and fields can be defined without assigning them values, however, they MUST always be assigned some value before label or field is accessed.

For static fields, value must be assigned in static {} initializer of the class.

For type members, value must be assigned before the type member is used (read).

Usage of un-initialized field will result in compile time error.

For labels, label must be assigned before the label is accessed.

All execution branches must result in label assignment. Usage of un-initialized label will result in compile time error.

This rule also applies to Nullable labels.

String value encoding

String value encoding is, by default, UTF-8. Custom default encoding can be set in compile time, or for each String object specifically using String type constructor. Strings in Franca are actually thin wrappers around byte arrays, and therefore can theoretically hold Strings encoded using any encoding supported by the system.

All String objects carry extra pointer to value of Encoding enum that indicates how the contents of the string are encoding during runtime.

Deterministic binary serialization of strings

Franca has special API to String and Char types to deal with binary serialization - instance methods String::getPortableBytes() and Point::getPortableBytes(), and static methods String.fromPortableBytes(...), and Point.fromPortableBytes(...).

In contrast to String::getBytes() and Point::getBytes(), and similar, these methods emit and accept Strings expressed as byte arrays, where byte arrays are prefixed with 2-byte numerical reference to encoding that was used to encode them.

This API can be used to safely store strings in external storage in binary form and later retrieve them from such storage without worrying about encoding correctness.

Design note: this approach is designed to solve very specific edge case, where working with different encodings in a program is commonplace. If possible, you should avoid having this issue in the first place.

Char type and strings

Individual logical parts of a String are referred to as chars in Franca and expressed as Char type. Franca Char differs slightly from implementations often found in other languages - Char type in Franca is in fact a kind of String with fixed maximum size of 1 logical character.

Char is defined to represent single logical unit of any string, and is therefore, variable length. Byte size of a char depends on the character encoding used for the particular string.

API of the String type exposes two sets of methods that are often used to modify strings, one for modifying strings relative to the byte size of the string, and one relative to the char size of the string.

For example:

String value = new String('😊😊', Encoding.UTF8)
value.byteSize() # 8 bytes
value.charSize() # 2 chars

# ...

value.charAt(2) # Char('😊')
value.byteAt(2) # Byte(0x9F)

Inline strings

There are 4 different ways to define a string.

Enclosed in single quotes ('text') - plain string. Enclosed in double quotes ("text ${field}") - template string. Evaluated for variables. Heredoc with name in single quotes (<< 'EOF' text EOF;) - plain multiline string. Heredoc without name in quotes (<< EOF text ${field} EOF;) - template multiline string.

Important: Strings in heredoc format have first and last newlines trimmed after creation. Always.

Language structures

Types

Type is the main building block in Franca and defines runtime object composition - methods, fields, etc. Types are similar to classes in other programming languages.

Interfaces

Franca supports interfaces with multiple inheritance (interface can itself extend one or more other interfaces). Interfaces can define method signatures, but not their behavior. There is no support for default behaviors, static methods or static variables in Franca interfaces. Interfaces can not import partials.

In Franca, interfaces CAN require presence of constructor(s) in types implementing the interface. In Franca, type constructors are considered part of type public API and can be therefore required by interfaces.

In Franca, interfaces CAN require presence of both public and protected methods - protected methods are considered part of the protected API of a type, but is considered public in a sense that it is still visible outside the immediate type definition, and therefore protected methods can be required to be implemented by an interface to enforce open-ness to extension or.

Example:

interface MyWorkerInterface
  work:
  work:
    -> String[] args

Functional interfaces

Functional interfaces allows for definition of single-method callable types. You can either create your own functional interfaces or use generic std.Function<T...args>.

Example:

functional Consumer<T>
  consume:
    -> T value
Implementing functional interfaces

Functional interfaces can be implemented either using types, or inline, as shown bellow:

package app

external type Main:
    external main:
        Function<String, Int32> baz = {
            -> stringLabel # Type implied String
            -> intLabel # Type implied - Int32
        }

        Function<String> bar = {
            # Explicit argument definition is not required for single argument functions, use special `it` label.
            it.foobar()
        }

        Some.Type.call({
            -> arg1
            -> arg2
            # Do something
            somevar
        })

Enums

Enum structure, or enumeration is a static set of types inherent to the enum type itself. Enum type can have any number of enumerated values defined that are effectively types extending enum base type. Enum types are static and can not have constructors or destructors, can not hold state, can not have fields and enums are always serializable.

Enums are effectively final, can not be extended or extend other enums.

Example:

external enum TimeZones
  UTC:
  GMT:
  CET:

Enum base type can contain abstract and implemented methods - abstract methods must be implemented by all enum members, and non-abstract methods are inherited by all enum members.

Example:

external enum TimeZones:
  UTC:
    String getName:
      'UTC'

  GMT:
    String getName:
      'GMT'
  CET:
    String getName:
      'CET'

  abstract String getName

  String getNameLowerCase
    getName().toLowerCase()

Enums, enum methods and enum members follow the same visibility rules as any type does.

Example:

external enum Baz:
  public BAR:
  public FAZ:
  external FUZ:

Annotations

Franca language has built in metadata system - annotations. Annotations are designed to contain service information and metadata for the constructs annotated with said annotations.

Values stored in annotations must be static. In runtime, annotations are read-only.

Example of a custom annotation:

@AnnotationTarget(AnnotationTarget.METHOD)
external public annotation MethodHandler:
  String[]? methods default null
  Bool enabled default true
Predefined annotations

@Deprecated - indicates component is deprecated. Will result in compiler warning when used. @Unstable - indicates component is unstable and not to be used in production. Will result in compiler warning when used. @TODO - indicates incomplete implementation. Will result in compiler warning when used.

Generics

Generic allow for types to serve as runtime containers for types that are not known at the time of definition of the type.

public type Foo<T, V>:
    V bar:
        noop
public type Bar:
    <T> T baz:
        -> Object baz
        baz as T

Partials

Partials are special kind of abstract types that can hold certain implementations of methods and can be used to construct other types.

In contrast to abstract types, partials are completely stateless.

Partials follow the same visibility rules as types, partials can extend other partials but can not implement interfaces.

Partials can not hold static fields or any other state.

Methods defined within partials follow the same visibility rules as methods in normal types.

Types must import a complete partial, it is not possible to single out certain methods from a partial and import selectively.

Example:

public partial MyPartial:
  extends OtherPartial

  public Bool foo():
    true

  public Bool bar():
    !foo()

type MyType
  imports MyPartial

#...
MyType baz = new MyType()
baz.foo() # true
baz.bar() # false

Transport types and rich data structures

Transport type structures is one of the most powerful features of Franca language. It allows defining complex nested data structures composed of types already defined and types automatically defined within the transport itself.

In essence, transport types allows you to define typed, nested structures for data transport use.

This is extremely useful when, for example, working with external document formats, like JSON etc, for schema definition and other tasks requiring complex nested data types.

Transport types are inverse of abstract types. Transport types can only hold fields with getters and setters, and cast, and operator overloads.

Transport types can not have delegates, methods, constructors and destructors. Transport types can not implement interfaces or import partials.

Example of a basic transport type:

transport MyDataTransport:
  mut String fooBar:
    get:
      'moo'
    set: # Requires field to be mutable.
      print(it)

Example of transport type with auto-definition:

transport ExtendedTransport:
    mut String foo
    mut UInt32 bar

    # This transport type is defined inline
    auto mut NestedTransport buz:
        UInt32 x
        UInt32 y

        # As is this
        auto mut Coord faz:
            String? maz
            CustomType baz

        # Array of...
        auto mut Thing[] boo:
            String? bing

Execution entry point

Entry point for applications is defined as zero-argument externally visible method called main. Bot the type and main method must be externally visible.

package app

external type Main:
    external main:
        println('Hello world')

Assuming the type name for this main method is Main, the address of this main method is app.Main.

Package naming conventions

Package should always mirror the location of the directory relative to the source tree root of the current asembly.

Package names must:

  • be written in lower case; and
  • be at least 1 character long; and
  • start with an ASCII alpha character; and
  • contain only ASCII alphanumerical characters and underscores, matching [a-z0-9_].

For example:

Source file src/com/example/product/warehouse/Item.fra
Source root: src/
Source file: Item.fra
Type: Item
Package: com.example.product.warehouse
FQCN: com.example.product.warehouse.Item
For libraries

Package names should reflect the identity of the author and identity of the program being written. Organizations should use reverse of their domain name for their packages and individuals should follow similar convention where possible.

Example root packages:

com.example.product
com.github.example_user.project
com.jhondoe.my_library

Where above convention is not possible to follow for any reason (ex. you don’t have or need a domain name), you can use local. namespace, for example, local.myorganization.some_project.

Publishing of packages using local. in Franca public vendor repository is not allowed, however, you can always publish such packages privately.

For applications

Standard package name for applications that are standalone, can not be embedded and result in final assembly (executable) is app.

app package MUST NEVER be used in libraries, for any reason.

Special use and reserved package names.

std.* package is reserved for the standard library std.vendor.* package is reserved for compiler vendor extensions for different language implementations.

Compile time dependency injection

Franca language has a built-in compile time dependency injection system.

Dependency-injectable types are, by default, singletons within their defined scope and by their reference name, if any.

Circular dependencies result in compile time error.

Dependency injection and enter keyword

enter keyword command allows for dependency injection of an object in another object. Using enter effectively links referenced dependency within current object instance as an immutable field.

Optionally, it is possible to indicate a named or scoped dependency target. If not set, scope default is used, and name of the dependency is also default.

It is possible to request creation of an instance-specific dependency using local keyword. Local injection points always create new instance of the dependency to be injected, instead of using the scope and name specific singleton.

Dependencies are injected during object instantiation, before any constructor calls.

Dependency creation is always lazy and dependency instances are created on first-real-use. Merely having or passing a reference to a dependency does not result in dependency instantiation.

Tags

It is possible to inject multiple dependencies of the same provided type (interface) by using tags. Tags allow for multiple dependencies implementing the same interface to be injected as, and referenced by, using Array container. Each provider can be defined to provide with both name and zero or more tags, and the provided instance will be resolvable by each individual tag, as well as its name.

It is not possible to inject dependencies member of different tags using enter.

Attempting to inject tag with zero members will result in a compile time error.

It is possible for tagged injection point to be marked local - in this case, new instances for each individual dependency will be created on first real use.

Tags are scope-aware and follow the exact same scoping rules as non-tagged injection points.

Syntax

To inject a single instance:

enter [local] <visibility> <Type|Interface> <label> [from <?<prefix.>scope>] [named <name>]

To inject all instances declared with a specific tag:

enter [local] <visibility> <Type|Interface>[] <label> [from <?<prefix.>scope>] [tagged <tag>]

Example
type Foo:
    public Bool foo:
        true

# ...

type Bar:
    enter private Foo foo # Assembly-specific default singleton of Foo injected automatically

    public Foo getFoo:
        foo

Scoping prefixes and vendor libraries

Code foreign to the current assembly, such as vendor libraries, are always prefixed using scope alias defined by the library import during linking, and are never provided in the default scope.

For example, if a library com.example.foo is linked as foo, and has a dependency provider that defines Bar type in scope zed, the injection point definition would be as follows:

type Baz:
    enter private Bar bar from foo.zed # `foo.` indicates foreign scope aliased during linking
Auto-wiring

It is possible to inject a global default singleton instance of any type, provided it:

  1. Has one zero-argument constructor, or zero constructors (implicit zero-argument constructor)
  2. It is visible in current scope

All other dependency configurations require use of providers.

Providers

Providers allow for provisioning of a type for dependency injection where creation of an instance requires some form of customization that is not resolvable by means of auto-wiring.

Providers are defined inside types. Providers are global to the runtime.

Providers are special language constructs and can reference other dependencies within the provider code block using enter keyword.

It is guaranteed that the by the time provider is called, all dependencies are already resolved.

Providers are called in static scope of the type and have no reference to this.

All provider dependencies must be defined inside the provide block.

Types constructed using providers can still have internal dependencies using enter blocks, as any other type would.

Foreign dependency providers

It is possible to define a dependency for assembly-foreign code, such as vendor library.

This is done using optional to directive in provide statement. Providers from local assembly always take precedence over providers in foreign assembly, meaning, defining a provider in local assembly will override equal provider if defined in the foreign assembly.

For example, if a library com.example.foo is linked as foo, and has a dependency for Bar interface in scope zed, the provider definition would be as follows:

type BarImplementation:
    interface Bar

    provide in zed as Bar to foo:
        new BarImplementation()

Providers that redefine foreign dependency can locally inject previous dependency definition using enter statement. This allows for redefinition of original dependency with, for example, a wrapper that still makes use of the dependency provided in the foreign code.

Example provider syntax
type Foo:
    provide [in <scope>] [named <name>] [as Interface] [to <linked prefix>] [tag <tag[,tag]>]:
        enter Config config # Inject a dependency required to create Foo

        new Foo(config.property)
Scope and dependency naming

Scopes and dependency names must:

  • be written in lower case; and
  • be at least 1 character long; and
  • start with an ASCII alpha character; and
  • contain only ASCII alphanumerical characters and underscores, matching [a-z0-9_].

Constructors

Franca types can have zero or more constructors defined per each type.

Constructors are invoked whenever an object is created from type, and all types have default zero-argument constructor unless an explicit constructor is defined in a type.

During object creation, only the topmost constructor is invoked, if the type happens to be a child of some other type.

Unless the parent type has no explicit constructors, child constructor MUST invoke parent constructor as first operation of the child constructor.

Constructors can have visibility, as any other method. By default, constructors inherit visibility of a type.

Destructors

Types can have zero or one destructor defined in type definition.

Destructor is a special method that is invoked right before the object is garbage collected from memory during graceful shutdown or normal operation of a program.

Once invoked, it is guaranteed that type of the destructor is not accessible outside of the destructor context.

Destructors are designed to allow for program to gracefully dispose of resources and shutdown correctly before being de-allocated from memory.

Destructors MIGHT not be invoked during abnormal program termination or if object is never garbage collected.

If type extends other types, parent destructors are invoked first, honoring inheritance depth and order. For example, if Foo extends Bar, and Bar extends Baz, the destructors will be invoked in this order:

  1. Baz
  2. Bar
  3. Foo

It is impossible to invoke destructor manually short of de-referencing the object from memory.

Overloading

Franca supports method overloading, as long as the number of arguments, or the type of arguments in order is different.

Overloading language features

Overloading target type resolution rules

Overloads will try to match subjects by inheritance and signature trying the most specific matches first. For example, if there is an overload for both specific type and interface the type implements, the overload that matches the type will be used. If there is no overload for specific type, but there is for an interface, the interface overload will be used, and so forth.

You can disable the resolution by using strict keyword on overload implementation definition - then, the defined overload will match the types exactly, and overload will only work for subjects with exactly the defined type.

Defining strict overload or strict cast on interface or abstract class is not possible and will result in compile error.

Operator overloading

Franca language supports limited operator overloading in local scope - you can overload an operator in the left-hand side object, and configure it to accept right hand side object as argument to the overloaded operator.

Operator overloads are evaluated left to right - only the left-hand side object must have appropriate overload for overloads to work.

Overloads can be defined in both types and partials for import.

By convention, changing the order arguments should generally follow math rules for the operator being overloaded, if any. As a basic example, we know that changing order of arguments for addition has the same result (1 + 3 === 3 + 1). It is language convention that operator overloads should follow the same logic, if overloads are implemented in both sides of the operation. If such is the case that logic is different, it should be clearly documented.

Operator overloading is defined by adding a special overload method to a type with defined return type and operator argument.

Operator overloads are not allowed to throw checked exceptions.

Overload methods are aware of the scope and have access to this.

Operator overloads follow all normal visibility rules any other method has. By default, operator overloads inherit visibility of the defining type.

type MyOverloadType
    overload MyOverloadType add:
        -> MyOverloadType other
        # TODO implement overload

    overload MyOverloadType divide:
        -> MyOverloadType other
        # TODO implement overload

    overload MyOverloadType divide:
        -> SomeOtherOverLoadType other
        # TODO implement overload

    overload MyOverloadType append:
        -> SomeOtherOverLoadType other
        # TODO implement overload
}
Cast overloading

Franca allows for custom implementation of casts from one type to another.

Cast overloads are aware of the scope and have access to this.

Cast overloads are not allowed to throw checked exceptions.

Cast overloads follow all normal visibility rules any other method has.

By default, cast overloads inherit visibility of the defining type.

type MyCastableType:
  cast String:
    'This object is ' + toString()

#...
MyCastableType foo = new MyCastableType()
String asString = foo as String # Works fine


#...
MyCastableType foo = new MyCastableType()
String asString = foo # Implicit cast. Also works fine.

Method delegation

Franca allows for shorthand method delegation to objects.

Delegation is only allowed to non-optional, immutable fields.

Delegation is explicit - you must list all methods delegated to a field

From compiler point of view, delegate methods are no different from normal methods - they follow the same rules for visibility, interface implementation and others.

Delegate methods can override visibility of delegated-to methods locally, but methods being delegated to must be accessible from current type.

type MyType:
  enter OtherType foo

  delegate foo:
    external public getBaz
    getBar
}

Event system

Franca comes with a built-in event system out of the box. Event system allows any type to emit any other type as event to outside listeners.

Event emission is synchronous and sequential process. Event listeners will be invoked in the same order they were registered to the particular object.

Event listeners are not allowed to throw checked exceptions and are generally discouraged from throwing exceptions of any kind. It is best to handle exceptions in event listeners gracefully.

All exceptions thrown inside event listeners during execution will propagate up the stack like any normal method call would.

Registering listeners to undefined events will result in compile time error.

type MyEvent:
 # Some event container

type MyObject:
  event MyEvent myFooEvent

  doSomething():
    emit new MyEvent() to myFooEvent

external type Main:
  external main:
    MyObject foo = new MyObject()

    on foo.myFooEvent:
      println(it)

    on foo.myFooEvent:
      println(it)

    foo.doSomething()

Scope keywords

this - current instance of current type. Can be used as variable or type hint. static - static reference to current type (like this, but in static scope). parent - during inheritance, parent object if any. parent does not exist if type does not extend other type, this will be a compile error. origin - reference to instance of invoking type. If a invokes b, inside b value origin will refer to instance of a. Type of origin is always Optional<Object> and must be explicitly cast during runtime where needed. origin exists only during an invocation, it is always Optional<Object>. Origin is ALWAYS determined on call time, even in lambdas. For example, if lambda is defined and passed around as reference, this will point to the type where lambda was defined. In contract, origin will only be populated when lambda is invoked and will contain a pointer to the previous type in call stack.

Comparisons

== is equal to (structural, same property signature, or with equals implementation) === is the same object as (referential, is the SAME object)

Pipelines

Franca supports inline pipeline calls - syntax feature that chains invocations of multiple functions with ability to pass output of one method into another.

Pipelines do require explicit method call definitions, to support for chaining of methods where input arguments might not exactly match the output of the previous call. it virtual label is used to hold the return value of the previous call.

Pipelines are chained calls of methods from the current scope. Do not confuse this with chained calls against return values, for example foo().baz().bar.faz().

type MyObject:
  String doSomething():
    'Hello '

  String doOtherThing:
    -> String value
    value + 'world'

  String doMoreThings:
    -> String value
    -> String arbitrary

    value + arbitrary

  run:
    String result = doSomething() and
        then doOtherThing(it) # value `it` contains the return value of `doSomething()`
        then doMoreThings(it, '!')

    println(result) # Hello world!

Unions

Unions is a special construct that allows for multi-type containers as a language construct.

Example:

Union<String, Boolean, Int32> bar = ['foo', true, 123]

String baz = bar[0]
Boolean faz = bar[1]
Int32 maz = bar[2]

Array and Union unwrapping

This feature allows for array and union unwrapping into local labels.

    Union<String, Boolean, Int32> bar = ['foo', true, 123]
    [String baz, Boolean faz, Int32 maz] = bar

Conditionals

if x === 3:
  return x

# ...

if 'foo' === x:
  return 'bar'
else if 'maz' === x:
  return 'baz'
else
  return 'ziz'

# Guard blocks
# expression <if> condition

return x if 'foo' === x
continue if 'foo' === x
break if 'foo' === x
throw new Exception() if foo === 'x'

# conditional invocation

call_function() if 'bar' === x

# Conditional assignment

String x =
    if 'foo' === y:
        'value'
    else if `baz' === y:
        'other value'
    else:
        'fallthrough'

Loops

while(true):
    # do something

dowhile(true):
    # something

for value in iterable:
    # something

for (key, value) in iterable_of_tuples:
    # something

for X in 0..10:
    # do [0,...10]

<iterable>.each:
 # it*

Features found in other languages explicitly excluded by design

These features and properties often found in other languages are excluded from Franca by design, for one reason or another. They are not expected to ever become part of the language.

  • Extension functions and properties. Types are defined as-is and are immutable.
  • Smart casts.
  • Default and named arguments.
© Matīss Treinis 2022, all rights, some wrongs and most of the lefts reserved.
Unless explicitly stated otherwise, this article is licensed under a Creative Commons Attribution 4.0 International License.
All software code samples available in this page as part of the article content (code snippets and similar) are licensed under the terms and conditions of Apache License, version 2.0.