Skip to content

Latest commit

 

History

389 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

QuantumC (C⁴/C^4)

The 4th Evolution of C

QuantumC (C⁴) is a general-purpose systems programming language. It is not a quantum computing language. It does not simulate quantum logic, compile to quantum circuits, or run on quantum hardware. QuantumC (C^4) is unrelated to the C99-to-OpenQASM compiler, QuantumC

int main() {
    `qout("Hello, World!");
    return 0;
}

Check the web-demo out at learnhardcode.dpdns.org/QuantumC/qc.html. Or go to the docs right here.

More Powerful Than Explosives™/j

C⁴ combines explicitness and simplicity to produce readable code, improving both DX and UX. Code should still make sense next week, even to the person who wrote it.


What is QuantumC?

QuantumC (C⁴/C^4) is a compiled, statically typed C-Style Systems programming language, focusing on clean code and low global scope pollution.

Features

  • Modern Syntax - Clean, intuitive, no boilerplate (other than main)
  • Functions & Lambdas - First-class functions with fn syntax for lambdas and type name(args-type args-name) syntax for normal functions
  • Multi-Return Values - Return multiple values without structs
  • Type Safety - Strong typing with auto inference
  • Quantum Booleans - Superposition of true/false
  • F-Strings - Python-style string interpolation
  • Special class methods - Representation, evaluation, and operator overloading

Video

Go to the docs

Quick Start

Install the binaries from GitHub, use the Package & Version Manager, or:

Installation

Dependencies

You must have clang installed (or the path /usr/lib/clang//include/unwind.h must exist) Also, if cloning a binary and not building from source, you must have the current latest libstdc++ (if on linux).


After cloning the repo to your machine, run.

cd QuantumC
chmod +x install.sh
./install.sh
# Now, you can type
qc [flags] (filepath)
# to run a file
# or
qc [flags]
# to enter the REPL

Sections


Versioning Scheme

QuantumC uses the following versioning scheme: cMa.Mo.MiP , where c is critical, for massive additions, such as the compiler being added, Ma being major versions, tracking large collections of features, Mo being moderate versions, tracking collections of similar features, Mi being minor versions, which track individual feature milestones within the current moderate version's theme, and P being the patch version. For the version x1.2.34 c = x Ma = 1 Mo = 2 Mi = 3 P = 4

P is omitted if it is 0. Critical versions represent the largest generational milestones in QuantumC's development.

v = Interpreter x = Compiler (Current) f = Feature-complete compiler s = Self-hosted compiler

Critical versions are intentionally rare and denote architectural milestones, not language features.

Development toward future critical versions may begin before the current critical version is complete. Multiple critical generations may therefore be in development simultaneously.

Minor (Mi) is always a single decimal digit (0-9). Once a minor version reaches 9, the next release increments the moderate version instead.

Unlike semantic versioning, QuantumC versions describe the scale and category of language evolution rather than API compatibility.

Development Status

Current Version: x1.0.1 = "Modifiers & Atomics" Next Version: x1.0.1 = "More Error Stuff"

Current Version Highlights

Critical
└─ Added Compiler

Major
└─ N/A

Moderate
└─ Modifiers

Minor
└─ N/A

Patch
└─ Concepts were jank

Recent Deprecations / Breaking Changes

These are deprecations in the past 3 moderate versions (x0.26.* -> x1.0.*)

Self parameter is now a pointer. This now requires explicit dereference (must use -> instead of .) Concepts as constraints must start with proves

Feature Roadmap

Category Feature Status
Core Logic Variables & Types (int, float, double, string, char, bool) Done
Constants (const) & auto Inference Done
Long and Short types Done
Operators Standard Math & Expressions (includes #^ power operator) Done
Control Flow (if/else, switch, while, for, foreach) Done
Functions Functions with Default Parameters Done
Lambdas & Higher-Order Functions Done
Native Multi-Return Values Done
Data Structures Arrays (with .length member) Done
Spread Syntax for Arrays (@) Done
Enums Done
Advanced Structs, Classes, & Namespaces Done
Union Types (TypeScript-style) Done
Advanced OOP & Operator Overloading Done
Manual Memory Management Done
System F-Strings (Python-style interpolation) Done
Random Number Generation Done
Stdlib Part 1 & 2 Done
Future Stdlib Part 3 Planned
Inline ASM Done
Generics Done
Classes Done
Structs Done
Unions Done
Functions & Methods Done
Variadic Generic Arguments Planned
restrict, out, inout, volatile Done
Extern Done
Bitwise Logic Done
Really fancy operator overloads Done
Try/Catch and throw Done
Error message quality and helpfulness upgrade Done

See the full list of remaining features in the roadmap.

Contributing

Found a bug? Have a feature request? Open an Issue!!

Want to contribute? PRs welcome!


License

MIT License - See LICENSE for details


i2 Primitive Type

qbool qb = both;
qif (qb && qtrue /* evaluates to both */) {
    `qout("True path");   // Executes
} qelse {
    `qout("False path");
}


Manual Memory Management

int main() {
    int* ptr = `malloc(sizeof "int");
    *ptr = 42;
    `qout("%p", ptr); 
    `free(ptr);
}

Include system

namespace Exported {
/*
Exported namespaces are merged during include resolution.
See the include-system docs for full details.
*/
    #include <Math, std> // std is an alias for ~/.qc/lib/stdlib.qc. This line imports the Math namespace from the standard library.
}

int main() {
    return Math::Max(1234, 432); // Using the math namespace.
}

Namespaces can also declare dependencies on other namespaces in the same file using #depends, ensuring includers automatically see required types even if they only explicitly included one namespace from that file:

#depends(x: y)
namespace x { /* uses y::Something internally */ }
namespace y { /* ... */ }

Want to learn more? Check out the docs for it.


Why QuantumC?

Feature C++ Zig Rust QuantumC
Total Runtime Medium Medium Medium Medium
Compile Time (relative) Slow Medium Medium Medium
Runtime Fast Medium Medium Fast
Memory safety Manual GPA Borrow checker Manual
Multi-return Structs Tuples Tuples Native
Generics Templates + Concepts Type as Argument Trait Based Constraint-Based

Based on the last reliable benchmark results, QuantumC showed performance in the same general range as C++, while offering a similar set of quality-of-life improvements found in languages such as Zig. Current benchmarks are being improved, and results should be considered preliminary.


Conventions

QuantumC has unusual naming conventions:

Type Convention Why?
Variables snake_case It's familiar to Python devs who changed their ways, C++, C, Zig, Go, and Rust devs.
Functions camelCase It allows for instant knowledge between if an identifier is a var, or function (lambdas use var casing, not function casing)
User Types PascalCase It is common across basically every programming language.
Constants SCREAMING_SNAKE_CASE Same as above.
Private Member Variables __snake_case Variable case prepended with __. Most underscores.
Protected Member Variables _snake_case Less underscores.
Protected Methods __camelCase Unique casing, more underscores.
Private Methods camel_Snake_Case Function casing, more underscores.
Namespaces PascalCase Same as user types.
Namespaces Not Meant For Inclusion Pascal_Snake_Case Unique casing style, more underscores, you have to be trying to include this.
Global Scope Functions camel_Snake_Case Unique casing style, more underscores, similarity to private methods is intentional, because global scope cannot be included.
Methods Used By Compiler _camelCase Different from everything else. (these methods are iterators and stuff. Methods you define and compiler uses)
Compiler Reserved _qc_, __qc_ and qc_ Unique, hard to use accidently
Compiler Intrinsics ` + snake_case Unique, impossible to use accidently

Max line size is around 120 relative to your starting indentation, tabs or spaces, lf newlines, comments are //, doc comments are ///, and top-level doc comments are //!. File paths are unquoted, everything other than main should go in a namespace when applicable, and namespaces should fit the following rules:

  1. Namespaces should do one thing well, similar to the UNIX philosophy,

  2. Namespaces should have either:

     1. one type or group of tightly related types such as bigints and their core helpers,
     2. above + namespaces containing extra helpers
     3. helper functions / utility functions (think a `Math` namespace with log, cos...)
     4. OR anything if directly mapping  C/C++/Zig/Rust code to C^4
    
  3. Types in namespaces should have short names: The namespace should have the longer name e.g.

namespace Array {
    class Arr<T, int S = 0> {
        ...
    }
}

Pointer asterisks bind to the type rather than the variable. The final * belongs to the declarator, unless it's a function return type. Then it's all on the type.

int** *x;
int* ptr_add(int *p) ...

Files are kebab-case. This is optional. QuantumC naming conventions are designed to make code readable without requiring the reader to inspect library code. Names should provide immediate context. My rule: RTFM once, not RTMSCE5S (Read The Manual and Source Code Every 5 Seconds), and these conventions make things hard to forget or mis-type, unlike C/C++ where every library uses entirely different conventions.

Example:

namespace Network {
    class Client {
        string server_name;

        void connectToServer() {
            ...
        }
    }
}
namespace Not_Embezzeling { // Intentionally formatted as a non-inclusion namespace.
    // Sure, you may not want to type all of that. That means your users absolutely don't.
    ..
}

Ideals

QuantumC follows four core rules:

  • Forced Cleanliness: QuantumC is designed to reward readable code. Language features should make the obvious solution the clean solution. Clean is not defined as 'Convenient for language', it means what it should be. Clean is not a "pythonic" equivalent; it is self-explanatory.
  • Your Memory, Your Problem: QuantumC does not prevent dangerous code. It expects the programmer to understand the consequences. If you want, you can write a segfault handler with a segfault in it. QuantumC is strongly typed, but union types are designed to be ergonomic rather than restrictive. Unlike Rust or TypeScript, QuantumC does not force exhaustive narrowing before every union operation. I will give you a loaded shotgun. If you blow your leg off, don't blame the gunsmith.
  • No Hiding: QuantumC is an explicit language, so your code does what it looks like it does. Nothing is hidden inside the parser while pretending to be stdlib, nothing is hidden away in some back catacomb. If it is an intrinsic, it says it is.
  • No Excessive Syntax: No capture lists on lambdas, no templates, no infinite <>, no Rust "bird droppings", no ! and @ everywhere.

Compiler Architecture

QuantumC uses a classic multi-pass compilation pipeline:

  1. Lexical Analysis / Preprocessing: Custom lexer converts text to tokens in one loop.
  2. AST Parsing: Recursive descent parser generating a strongly typed Abstract Syntax Tree.
  3. Type Checking & Semantic Analysis: Resolves user-defined types, namespaces, and TypeScript-style union types. The unique thing is that this pass is merged with the compilation/codegen phase
  4. Intermediate Representation (IR): Generates LLVM IR. Target-aware pointer arithmetic is achieved dynamically via target-specific DataLayout queries.
  5. Codegen: Emits native platform object files (.o) or WebAssembly binaries via LLVM's target machines.

Multi Return

int, string GetStatus() {
    return 200, "Success";
}
int main() {
    int code, string alias = GetStatus();
    `qout("%s", f"Code: {code}, AKA: {alias}\n");
    return 0;
}

Type collections and aliases

Define variables that can hold multiple types using a simple | syntax. The parser automatically distinguishes these from standard aliases:

int main() {
    // A Union Type (TypeScript-style)
    type IdT = int | string;
    IdT id = 101;
    id = "A101"; // Perfectly valid

    // A Standard Alias
    type UserID = int;
    UserID myId = 5;
}

C interop and inline ASM

Extern "C" is the only supported extern style, and thus no string is needed to say where you are externing to.

extern:
int add(int a, int b) {
    return a + b;
}
:extern

Extern is only for externalizing api. To use foreign code, you must put it in a foreign block.

foreign:
int do_some_c_stuff(int x);
:foreign

The QuantumC inline ASM syntax is like a simplified version of the GCC syntax:

inline(R"(
mov rax, 1
mov rdi, 1 
mov rsi, $0r ; argument 1 (Hello, World!) 
mov rdx, $1r ; argument 2 (14)
syscall
)"/* your inline asm string */, "Hello, World", 14, "~{rax,rdi,rsi,rdx}" /* clobbers */);  

You can also use AT&T ASM syntax by making sure the first 5 characters of your ASM string are

; ATT

Storage & Argument Specifiers

QuantumC has 4 special (non-const) storage modifiers.

  1. volatile Volatile means the compiler won't optimize it. It can be used before variable declarations, or on function definitions. On methods, it must go after the access modifier and final, and before the return type.
volatile void infinite_time() {
    while(true) {
    }
    return;
}
  1. restrict

restrict tells the compiler that this pointer is the exclusive access path to the referenced memory. Other unrelated pointers must not access the same memory in a way that violates the restrict contract. This allows more aggressive optimization.

Example:

void doSomePointer(int *restrict ptr, int *other) {
    *ptr = 10;
    *other = 20; // Undefined behavior if other points to the same memory as ptr
}
  1. out Out tells the compiler that this parameter is write-only, and this memory address will not be copied. It also only exists for optimization purposes.
void writeOnly(out int p) {
    p = 123; // OK
    int x = p; // ILLEGAL. Breaks contract.
    int *x = &p; // ILLEGAL also.
}
  1. inout Inout tells the compiler that this parameter will be read and written from, but its address will not be copied.
void rw(inout int p) {
    p = 123; // OK
    int x = p; // OK
    int *x = &p; // NOT OK
}

Variadic arguments

QuantumC variadic arguments look like this:

int add_all(...args) {
    int res = 0;
    while (!`is_empty(args)) {
        res += `next(args, "int");
    }
    return res;
}

args is a variadic argument (hence the ...), which can be passed any number of parameters. is_empty is a runtime compiler intrinsic that takes a variadic arg-list and returns whether it is empty. next is a runtime compiler intrinsic that takes a variadic arg-list and the expected type and returns the next element in the arg list cast to that type.

You may use C-Style variadic arguments too, but only in foreign blocks.

foreign:
void printf(char* fmt, ...);
:foreign

Bitwise logic

QuantumC has all the standard bitwise logic operators; however, it has a non-standard XOR and Right-Shift token. The Bitwise XOR operator in C^4 is $. It is $ because ^ and ^^ are already used tokens, and C^4 avoids repeating tokens to improve quick readability. The same logic applies for right-shift: |> is the right-shift token, because it allows the parser to immediately determine:

Node<Vector<X>> // Is this RSHIFT or ending generics?

This is easy to determine (because it would be illegal for rshift to be there); however, using this non-normal rshift operator allows it to be instant because:

Node<Vector<X>> // This is unrelated to RSHIFT

QuantumC also has a special operator for logical right shift: :>. This is because it allows avoiding constantly casting between signed and unsigned integer types, unlike C++'s "arithmetic if signed, logical if unsigned"

2 more unique bitwise things QuantumC does:

  1. Built-in rotations: <<< and |>> and LROT and RROT, allowing 1 instruction rotations instead of 6+.
    • and - have lower priority than shifts. 3 << 3 + 2 == 26. This is because the shift expression l << r is equivalent to l * 2 ^ r. If r was 2 + 2, that would be l * 2 ^ 2 + 2, which would be l * 4 + 2.

Generics

QuantumC generics have simple syntax:

class C<T> {
    T x;
    C() {
    }
}
int main() {
    C<int> thing = C();
}

Generics are allowed on structs, classes, concepts, unions, aliases, functions, and methods. The unique thing about QuantumC's generics is its constraint system: The constraint system follows this syntax:

<T([constraint]:[[!]<[typename]'|'...>]>

Main constraints are as follows:

usertype: non-primitive type
primitive: primitive type
pointer: any pointer type
numeric: any numeric type

So

<T(numeric:)>

Can be any numeric type. The subconstraint system is like this:

<T(:!int|string)>

This means "T can be any type other than int or string". The ! means not anything in this set, and the type|type means these types. QuantumC also has non-type generic parameters.

<int S> // S is a non-type generic parameter (a compile time int)

In Rust,

<T(numeric:)>

Would be

<T: std::ops::Add<Output = T> + std::ops::Sub<Output = T> + std::ops::Mul<Output = T> + std::ops::Div<Output = T> + PartialOrd + Copy>

And in C++, it would be

template <typename T>
requires std::is_arithmetic_v<T>
T

or in old SFINAE C++

template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
T

And in Zig:

...(comptime T: type, ...) ... {
    switch (@typeInfo(T)) {
        .Int, .Float => ...,
        else => @compileError("T must be numeric"),
    }
}

Iterators

Iterators work as follows: The class you want to iterate must define the following method:

MyIterator _begin() // Returns an iterator to the beginning of the class

_end is optional, and returns an iterator to the end of the class. The iterator itself MUST define the following 2 methods:

bool _atEnd() // Returns true if there is nothing left to iterate
T _next() // Returns the current value then moves to the next

and may define the following recommended extra methods:

bool _atStart() // Returns true if the iterator is currently at index 0
T _prev() // Returns the element at current index -- and decrements current index
void _moveTo(whateveryouwant idx) // sets index to idx
MyIterator<T> _map(fn(T elem) -> T) // preforms an operation on every element
MyClass<T> _collect() // returns the class that this iterator iterates

Example:

class ArrayIterator<T> {
    T* data;
    int size;
    int current_index;
    ArrayIterator(T* data, int size, bool is_end) {
        this.data = data;
        this.size = size;
        this.current_index = `ternary(is_end, size, 0);
    }
    bool _atEnd() {
        return this.size <= this.current_index;
    }
    T _next() {
        if (!this._atEnd()) {
            return this.data[this.current_index++];
        }
        return this.data[this.current_index];
    } 
    bool _atStart() {
        return this.current_index <= 0;
    }
    T _prev() {
        if (!this._atStart()) {
            return this.data[--this.current_index];
        }
        return this.data[this.current_index];
    }
    void _moveTo(int index) {
        if (index >= this.size) {
            index = this.size - 1;
        } else if (index < 0) {
            index = 0;
        }
        this.current_index = index;
    }
}
class Array<T, int S = 0> {
    T* data;
    int size;
    Array() {
        this.data = nullptr;
        this.size = 0;
    }
    void operator[]=(T* data, int length) {
        if (length > S) {
            this.size = length;
        } else {
            this.size = S;
        }
        this.data = `malloc(sizeof "T" * this.size);
        for (int i = 0; i < this.size; i++) {
            this.data[i] = data[i];
        }
    }
    T operator[](int index) {
        return this.data[index];
    }
    ArrayIterator<T> _begin() {
        return ArrayIterator<T>(this.data, this.size, false);
    }
    ArrayIterator<T> _end() {
        return ArrayIterator<T>(this.data, this.size, true);
    }
}

The standard library collections have iterators in the structure of this:

namespace Array
    namespace Iterator {
        class It {
            ...
        }
    }
    class Arr {
        Iterator::It _begin() {
            ...
        }
        ...
    }
}

Try/Catch and Error Returns

C^4 supports both try/catch exception handling and multireturn/unions for error handling. You are encouraged to use both, or combine them where appropriate. Just document if you throw or not. Example:

int main() {
    try {
        throw 123;
    } catch (int e) {
        ...
    }
}

Or for union-based:

struct myerror {
    ...
}
type MyResult = int | myerror;
MyResult myThing() {
    return myerror{...};
}

Defer

C^4 has defer, like Go and Zig. Unlike Go's defer, C^4 defer does not participate in stack unwinding. Deferred code is not automatically executed when an exception propagates out of a scope. This behavior is intentional: making defer participate in stack unwinding would introduce additional runtime overhead. defer should not be considered a destructor mechanism. It is a scope-exit convenience feature, not RAII.

Concepts

C^4's concepts are like a combination of Typescript interfaces, Rust traits, and C++ concepts.

concept Printable {
    1_of {
        void print();
        void print(Self self);
    }
    default {
class:
        void print() {
            `qout("Printing. . .");
        }
else:
        void print(Self self) {
            `qout ("Printing. . .");
        }
    }
}
class PDF {
    string data;
    PDF(string data) {
        this.data = data;
    }
    void print() {
        `qout("%s", this.data);
    }
}
class ASCII {
    string data;
    ASCII(string data) {
        this.data = data;
    }
}
struct Paper {
    string data;
}
type File = PDF | ASCII;
/// Print would print PDFs data
PDF proves Printable;
/// Print would print "Printing. . ."
ASCII proves Printable;
/// Print would print the paper's data
Paper proves Printable with_proof { // with_proof block to add additional definitions, like impl in Rust
    void print(Self self) {
        `qout("%s", self.data);
    }
}
/// Would print "Printing. . ."
File proves Printable;

The Self arguments are explicit for non-class methods added through concepts. This is to emphasize structs are not classes, and should stick to being POD. Concepts only allow methods on structs because it's better for DRY than having 50 functions with similar names.

Simple File Example

namespace Exported {
    #include<Vector, std>
}
int main() {
    Vector::Vec<int> my_vec = [1, 2, 3];
    my_vec.push(123);
    `qout("%i", my_vec[2]);
    return 0;
}

Self Hosted Runtime

There is currently an unknown bug with the self-hosted runtime.

Performance Comparison

Benchmarks are currently unreliable and show significant fluctuations between runs, or things like a 0-millisecond runtime in O1 but 30 in O3. Results should be treated as preliminary rather than definitive. A more robust benchmarking system with better workload scaling and measurement methodology is planned after version x1.0.0.

Known Limitations

&& and || do not short-circuit on unions. Volatile does not exist on structs or class fields, and does not work on property accesses. In general, volatile is not 100%.

Standard Library Namespaces:

Vector
List
Array
AdvQBool
Math
Utils
OSInterop

Made by Luca Fazio