Skip to content
PaskalCPP

PaskalCPP

Write Pascal-style code, use any C and C++ library.

A minimal, Pascal and Oberon inspired systems programming language that compiles to C++23. #include any C or C++ header and call it: no bindings, no wrappers, and the toolchain ships with it.

Free and open source, MPL-2.0 Windows x64 and Linux x64 Compiles to C++23 Toolchain in the box
The big idea

C and C++ are part of the language

In a PaskalCPP source file, PaskalCPP code and C++ code sit side by side, freely mixed, in the same file. To use a C or C++ library, you #include its header and call it. No bindings to write. No wrapper generator. No header translation.

Readable by design

Pascal and Oberon style: blocks open and close with words, := assigns, = compares, every control-flow construct closes with its own word (end, or until for repeat).

C++ is the surface

A C++ type can be the type of a PaskalCPP variable, a C++ expression can sit inside a PaskalCPP expression, a C++ declaration can stand at module level.

C++23 is the machine code

PaskalCPP writes a C++23 header and source for each module, and the bundled clang compiles them.

Errors speak PaskalCPP

A PaskalCPP mistake is reported by PaskalCPP; what clang or the linker finds is mapped back to your .pcpp file, line and column.

The toolchain is in the box

PaskalCPP ships its own Zig and clang toolchain. One command turns a .pcpp file into a native binary.

Use any C or C++ library

If C++ can use it, PaskalCPP can use it

A C or C++ library is used from PaskalCPP as it is, with no binding layer in between.

  • A library is used by #include-ing its header and calling it. Its names are opaque to PaskalCPP and checked by clang.
  • Any C or C++ type is a type, written as C++ spells it: unsigned int, std::vector<int>, struct tm.
  • C++ is written inside an expression as in C++: std::max<int>(3, 9), LV.size(), new, delete, nullptr, lambdas.
  • @linklibrary, @addlibrarypath and @addincludepath tell the build where a library lives, and @copydll puts a DLL beside the built program.
cpp_mixing_std.pcpp
module exe cpp_mixing_std;

#include <string>
#include <vector>
#include <cmath>
#include <algorithm>

template<typename T>
T Max2(T a, T b) {
  return a > b ? a : b;
}

var
  LS: std::string;
  LV: std::vector<int>;
  LD: float64;
  LX: int32;

begin
  LS := "hello world";
  if LS.find("world") <> std::string::npos then
    println("found at {}", LS.find("world"));
  end;

  LV.push_back(3);
  LV.push_back(1);
  LV.push_back(2);
  std::sort(LV.begin(), LV.end());
  println("sorted {} {} {}", LV[0], LV[1], LV[2]);
  println("size {}", LV.size());

  LX := 3;
  LD := std::pow(float64(LX), 2.0);
  LX := Max2(LX, int32(LD));
  println("max {}", LX);
end.
output on x86_64-windows-gnu and x86_64-linux-gnu
found at 6
sorted 1 2 3
size 3
max 9
[Heap] Allocs: 0, Frees: 0, Leaked: 0
Mixing in practice

Two languages, one file

At module level a C++ struct, function, template or global stands beside PaskalCPP declarations and is used by them. For the cases plain mixing cannot express there are three forms, each with one job.

cpp header; cpp source;

Where C++ goes

Chooses whether the plain C++ that follows is placed in the module's generated header or its source. The default is source.

cppstart source ... cppend;

A verbatim block

Macro-driven code, and in a body the C++ statements PaskalCPP has no form for. They use the PaskalCPP variables in scope.

cpp("1 + 2") * 3

A verbatim expression

Emitted in place and in parentheses, so cpp("1 + 2") * 3 is 9.

Own the boundary

PaskalCPP does just enough C++ understanding to find where C++ begins and ends, then gets out of the way. PaskalCPP owns PaskalCPP, clang owns C++, and nobody second-guesses anybody.

The language

A small, sharp set of constructs

The constructs PaskalCPP itself provides; C and C++ supply everything else.

Modules

Every source file is a module: an exe, a dll, a lib or a unit. import brings in PaskalCPP units, #include brings in C and C++. There is no project file and no build script.

Types

Seventeen builtin types, plus every C and C++ type. Records with extensions, packing, alignment and bitfields; overlays, choices, sets, static and dynamic arrays, pointers and routine types.

Objects as values

Methods, asked-for virtual dispatch, init and done, copy and move, own / ref / weak owners, interfaces, embeds, properties, operators, is and as, and derivation from C++ classes.

Exceptions

guard blocks with except and finally; a hardware fault inside a guard is caught as an exception and the program goes on, the same on both targets.

C linkage, both directions

clink gives a routine its plain, unmangled C name, so C code calls it, and anything that can call C can call it. external binds a routine to a symbol linked from elsewhere.

Unit testing built in

Test blocks follow the module's end.; @unitTestMode on; runs them with twelve asserts. The exit code is the number of failed tests, at most 255.

Your build lives in your source

Directives at the top of the main module set the target, the optimize level, libraries, include paths, the icon and version info, so one .pcpp file describes its own build.

No garbage collector

Memory is yours: raw blocks, records created and destroyed explicitly, dynamic arrays, objects held by own and ref, or anything C++ offers. A debug build counts heap blocks and owned objects, and names the line that built every object still alive at the end.

Real code

Checked on both targets

Every example on this page is a file built and run on x86_64-windows-gnu and x86_64-linux-gnu by its check; the output shown is the output it must print.

testing_runner.pcpp
module exe testing_runner;

@unitTestMode on;

import testing_unit;

var
  GHits: int32 = 0;
  GText: string = "a";

initialize
begin
  println("main init");
end;

finalize
begin
  println("main final");
end;

begin
  println("the body does not run in test mode");
end.

test "asserts that hold"
var
  LP: ptr to int32;
  LW: uint32;
  LI: int32;
begin
  LP := address of GHits;
  LW := 7;
  LI := 2;
  testAssertNotNil(LP);
  testAssertEqualInt(16, testing_unit.Square(4));
  testAssertEqualInt(7, LW);
  testAssertEqualFloat(2.0, LI);
  testAssertEqualFloat(0.3, 0.1 + 0.2);
  testAssertEqualStr("ab", GText + "b");
end;

test "asserts that fail"
begin
  testAssertEqualInt(10, testing_unit.Square(3));
  testFail("still runs after a failure");
end;

test "a raise ends the test"
begin
  raisecode(7, "stop");
  testFail("never reached");
end;

test "a fault ends the test"
var
  LQ: ptr to int32;
begin
  LQ := nil;
  LQ^ := 1;
  testFail("never reached");
end;

test "returns early"
begin
  GHits := GHits + 1;
  return;
  testFail("never reached");
end;
output on x86_64-windows-gnu and x86_64-linux-gnu, exit code 3
unit init
main init

╔══════════════════════════════════════════════════════════════╗
║                     Unit Test Runner                         ║
╚══════════════════════════════════════════════════════════════╝

Running 6 test(s)...

✅ PASS: unit squares
✅ PASS: asserts that hold
❌ FAIL: asserts that fail
  🔴 …/samples/testing_runner.pcpp:91: testAssertEqualInt failed: expected 10, got 9
  🔴 …/samples/testing_runner.pcpp:92: testFail: still runs after a failure
❌ FAIL: a raise ends the test
  🔴 …/samples/testing_runner.pcpp:95: test raised exception 7: stop
❌ FAIL: a fault ends the test
  🔴 …/samples/testing_runner.pcpp:101: test raised exception 3: Access violation
✅ PASS: returns early

════════════════════════════════════════════════════════════════
Results: 3 passed, 3 failed, 6 total
════════════════════════════════════════════════════════════════
main final
unit final
[Heap] Allocs: 0, Frees: 0, Leaked: 0

Unit testing built in

Testing is part of the language, not an add-on. This program imports a unit with tests of its own.

  • Test blocks, test "name" begin ... end;, follow the module's end. and see its routines, variables and imports.
  • @unitTestMode on; runs the initialize sections, every imported unit's tests and the exe's own, then the finalize sections; the main body does not run.
  • A failed assert records its line and the test goes on; a raise or a fault fails that test and the runner goes on.
  • The exit code is the number of failed tests, at most 255, the same on every target.
Safety nets C never had

C's power without C's classic traps

Every one of these rules is enforced by the compiler, on PaskalCPP code, where they belong. The C++ in your file follows C++ rules, and clang checks it.

  • No hidden conversions. A value converts implicitly only when every value it can hold fits the target exactly, in arithmetic too.
  • Constants are checked. An overflow, a zero divisor or a shift count out of range is an error, never a silent wrap.
  • One meaning per operator. := assigns and = compares, always; LX = 5; as a statement is an error.
  • Division says what it means. / is real division, so 7 / 2 is 3.5; div and mod are the integer forms.
  • No precedence traps. & and | bind like * and +: LA & 2 = 2 means (LA & 2) = 2.
  • Comparisons do not chain. a < b < c is an error, not a silent surprise.
  • Declare before use, everywhere. Mutual recursion is written with forward, checked against the full declaration.
expressions_power.pcpp
module exe expressions_power;

var
  LT: bool = true;
  LF: bool = false;
  LA: int32 = 6;
  LU: uint32 = 4;
  LC: char = 'a';
  LNeg: float64 = -3.75;
  LN8: int8 = 126;

begin
  println("shl before + {}", 1 + 2 shl 1);
  println("* before shl {}", 2 * 3 shl 1);
  println("& before = {}", LA & 2 = 2);
  println("not before = {}", not LT = LF);
  LC++;
  println("char step {}", LC);
  println("truncates {}", int32(LNeg));
  println("mixed sign {}", uint32(LA) + LU);
  println("test a bit {}", (LA & 1) <> 0);
  LN8 += 1;
  println("compound {}", LN8);
end.
output on x86_64-windows-gnu and x86_64-linux-gnu
shl before + 5
* before shl 12
& before = true
not before = true
char step b
truncates -3
mixed sign 10
test a bit false
compound 127
[Heap] Allocs: 0, Frees: 0, Leaked: 0
Errors that speak PaskalCPP

Reports point at your source

A PaskalCPP mistake is reported in PaskalCPP's words; what clang or the linker finds is mapped back to your .pcpp file, line and column.

PaskalCPP errors are PaskalCPP errors

A mistake on a PaskalCPP construct is reported by PaskalCPP itself, in its own words and types, with a note where one helps.

What only C++ can judge comes home

clang's and the linker's messages are mapped back to your .pcpp file, line and column; an undefined symbol is reported at the line that calls it.

One mistake, one report

A diagnostic is reported once per build; what PaskalCPP has already judged, clang never judges again.

LXthe lexer
PRthe parser
SMsemantics
ZBthe build and the toolchain
From a source file to a native binary

Zero external toolchain

The Zig toolchain, and the clang compiler inside it, ship with PaskalCPP. zig build caches compiles, so unchanged code is not recompiled.

  • Native executables, DLLs and static libraries for Windows x64 and Linux x64, built from one Windows machine, from the same source.
  • A Linux build runs from the same machine through WSL: pcpp <file> -r -t x86_64-linux-gnu builds it, runs it and shows its output.
  • On Windows the binary is stamped with your icon and version information; @copydll puts the DLLs it needs beside it.
  • The output is a plain native binary. No runtime to install, no virtual machine, no interpreter.
  • CMake
  • Makefiles
  • Build scripts
  • Header translation
  • Bindings
  • A project file
  1. PaskalCPP sourcemixed freely with C and C++
  2. PaskalCPP compilerwritten in Delphi
  3. C++23 header and sourceone pair per module
  4. Bundled Zig and clangthrough zig build
  5. Native exe, DLL or static libraryWindows or Linux
One command, every role

A development system, not just a transpiler

pcpp compiles, and it starts the other roles too. The compiler, the language server, the debugger and the editor share the compiler's own frontend: no second interpretation of PaskalCPP anywhere.

pcpp editor

The PaskalCPP Editor

An Electron application whose editing surface is Monaco. Build (Ctrl+B) and Run (Ctrl+F5) call pcpp with the toolbar's optimize level and target.

pcpp --lsp

The language server

Diagnostics as you type, hover with doc comments, go to definition, completion, inlay parameter names, the outline, workspace symbols, the call hierarchy and formatting. Any LSP client can use it.

pcpp --dap

The debugger

For x86_64-windows-gnu: breakpoints, continue, step over, into and out on PaskalCPP lines; variables and the call stack in PaskalCPP's names and types. pcpp <file> -d is the console debugger.

BuildCtrlB
RunCtrlF5
Start debugging / ContinueF5
Step over / Step intoF10F11
Go to symbol in workspaceCtrlT
Show call hierarchyShiftAltH
Format documentShiftAltF
Find / ReplaceCtrlF CtrlH
Watch and listen

See PaskalCPP in action

An intro, an infographic, a short, an explainer and a deep-dive podcast about PaskalCPP.

Who it is for

Anyone tired of writing bindings

Pascal and Delphi developers

Pascal and Oberon syntax, with every C and C++ library an #include away.

C++ developers

Pascal-style code beside your C++ in the same file, using the C++ libraries and templates you already use.

Systems programmers

Native binaries, pointers, raw memory and C linkage, with conversions and constants checked by the compiler.

Game developers

C and C++ game and multimedia libraries used by including their headers, with no bindings to maintain.

Library authors

Ship a C-linkage DLL or static library from a readable language.

Educators and learners

A systems language with Pascal-style syntax that reaches C and C++ directly.

Get started

From nothing to a native binary

PaskalCPP is free and open source under the Mozilla Public License 2.0. Releases, issues and the community live on GitHub.

  1. Get PaskalCPP

    Download the latest release from GitHub. The Zig and clang toolchain is inside; running a Linux build from Windows uses WSL.

  2. Write a module

    A module starts with its kind and name and ends with end.; the smallest program is an exe whose body prints one line.

  3. Build and run it

    pcpp hello.pcpp builds; -r builds and runs; -t picks the target. The debug build (the default) ends with the runtime's heap report.

  4. Open the editor

    pcpp editor starts the PaskalCPP Editor, with its language server and debugger.

  5. Read the docs

    The tour walks through a first program and the language; the documentation covers the language, the build, and the tools.

hello.pcpp
module exe hello;

begin
  println("Hello from PaskalCPP!");
end.
output on x86_64-windows-gnu and x86_64-linux-gnu
Hello from PaskalCPP!
[Heap] Allocs: 0, Frees: 0, Leaked: 0
Community

Build it with us

Share what you build, ask questions, report issues and help shape PaskalCPP.

The clarity of Pascal and the reach of C++, with nothing in between.

Every example on this page is built and run on both official targets.

PaskalCPP infographic