picceler
A compiled DSL for image processing, built on MLIR
Loading...
Searching...
No Matches
LANGUAGE

This document aims to provide information about the picceler language, more specifically it's syntax and builtin functionalities.

Syntax

Comments

# starts a comment that runs to the end of the line. There is no block-comment syntax.

# This whole line is a comment
image img = load_image("~/Pictures/cat.png") # so is everything after this '#'

Program structure

A picceler source file is a sequence of top-level statements. Two shapes are accepted:

  • One or more function definitions, one of which is named main — main is the entry point.
  • A sequence of statements with no main function — the compiler implicitly wraps them in a generated main for you (any other function definitions in the file are left as-is, only the non-function statements are moved into the generated main). This is why examples/photo_pipeline.pic and examples/blend_and_compare.pic can be flat scripts with no def main() at all.

Mixing the two — defining main explicitly and having other top-level statements outside of any function — is a compile error.

main's return value becomes the process exit code, so it's held to a stricter rule than other functions: declare -> int64 and return a value, or declare no return type at all (in which case return 0 is implicit) — any other declared return type is a compile error.

# explicit main, no implicit wrapping needed
def classify(float64 value) {
if (value < 10) {
print(" {} is a small number\n", value)
}
}
def main() {
print("-- picceler language tour --\n")
...
}
# examples/photo_pipeline.pic (excerpt) — no 'main' defined, gets implicitly wrapped
image photo = load_image("../img/cat.png")
show_image(photo)
image brightened = brightness(photo, 15)

Variables & assignment

Variables are statically typed: every assignment that introduces a name must be prefixed with an explicit type keyword (see "Type annotations" below). A name can only be declared once per scope — assigning to an already-declared name is a compile error, so variables are immutable once introduced.

type name = expr
string pathx = "~/Pictures/cat.png"
image img = load_image(pathx)

Literals

  • Numbers — float64 floating point values, with an optional leading - directly against the first digit (e.g. -1, 3.14, -0.5). There is currently no separate integer literal syntax.
  • Strings — double-quoted, e.g. "cat.png". Recognized escape sequences: \n (newline), \t (tab), \r (carriage return), \\ (backslash), \0 (null byte), plus escaped single and double quote characters. A leading ~ in a string literal is expanded to the user's home directory at parse time, so "~/Pictures/cat.png" works the same as it would in a shell.
  • Kernels — NxM matrices of numbers, every row must be the same length: [[1,2,3],[4,5,6],[7,8,9]].

Type annotations

int64, float64, string, image, and kernel are the recognized type names — kernel is always written with explicit dimensions, e.g. kernel<3,3>. Every variable declaration (see "Variables & assignment" above), function parameter, and function return type must carry one of these; there is no type inference.

Operators & expression precedence

From lowest to highest precedence:

  1. Relational: ==, !=, <, >, <=, >=
  2. Additive: +, -
  3. Multiplicative: *, /

Parentheses ( expr ) can be used to override precedence.

float64 root_val = sqrt(25.0)
float64 cubed = pow(2.0, 3.0)
float64 complex_calc = -1 + 2 * 3 + root_val + cubed

Current limitations:

  • There are no logical operators (&&, ||, !) — conditions are built from a single relational expression.
  • There is no unary negation operator for arbitrary expressions. -1 is a negative number literal (the - must be directly followed by a digit), but -x or -(a + b) are not valid — write 0 - x instead.

Function definitions & calls

def name(type1 param1, type2 param2, ...) -> returnType {
...
return expr
}

The parameter list and -> returnType are both optional (a function with no declared return type returns nothing). Calling a function uses the familiar name(arg1, arg2, ...) syntax.

# a function with a typed parameter, and calling it
def classify(float64 value) {
if (value < 10) {
print(" {} is a small number\n", value)
} else if (value < 20) {
print(" {} is a medium number\n", value)
} else {
print(" {} is a large number\n", value)
}
}
def main() {
classify(3.0)
}

Control flow

if (cond) { ... }, with optional else { ... } or chained else if (cond) { ... }:

if (complex_calc == 18){
print("Binary operations passed successfully! \n")
} else if (complex_calc == 0) {
print("Got zero\n")
} else {
print("Something else\n")
}

For loops

for (var = lowerBound .. upperBound [step stepExpr]) {
...
}

step defaults to 1 when omitted.

for (i = 1 .. 5) {
print("Binary operations passed successfully! \n")
}

Functionality

Builtin Functions

  • load_image(string) -> loads the image specified by string and assigns it to a variable
  • save_image(img, string) -> saves img to the path specified by string
  • show_image(img) -> opens a window to show img
  • read_number(string) -> prompts with string and reads a number (float64) from the keyboard
  • read_string(string) -> prompts with string and reads a string from the keyboard
  • print(string, ...) -> prints string to the console, substituting each {} placeholder in order with the remaining arguments, e.g. print("x = {}, y = {}\n", x, y)
  • sqrt(float64) -> square root of the argument.
  • pow(float64, float64) -> the first argument raised to the power of the second argument.

Builtin Operations

  • brightness(img, value) -> Eltwise addition of value to every pixel of img
  • invert(img) -> Eltwise inversion of every pixel of img, following the 255 - pixel formula.
  • convolution(img, kernel) -> Performs a convolution on img using the specified kernel
  • sharpen(img, value) -> Adjusts the contrast between adjacent pixels to increase the sharpness of img
  • box_blur(img, radius) -> Applies a simple box blur to img by averaging pixels within the given radius
  • gaussian_blur(img, radius) -> Applies a smooth, weighted gaussian blur to img based on the specified radius
  • edge_detect(img) -> Detects and highlights the outlines and edges within img
  • emboss(img) -> Applies a 3D effect to img by highlighting pixel intensity differences
  • rotate(img, angle) -> Rotates img by angle degrees (current implementation supports multiples of 90; positive is counter-clockwise, negative is clockwise). For 90/270 rotations, output dimensions are swapped to fit the rotated image so the full content is preserved (no clipping)
  • crop(img, x, y, width, height) -> Crops img to the widthxheight rectangle whose top-left corner is at (x, y)
  • dilate(img, radius) -> Expands bright regions of img using a radius-sized neighborhood, so foreground areas grow
  • erode(img, radius) -> Shrinks bright regions of img using a radius-sized neighborhood, so foreground areas contract
  • diff(img1, img2) -> Computes the pixel-wise difference between img1 and img2
  • blend(img1, img2, weight) -> Blends img1 and img2 together using weight as the mix factor

Builtin Types

  • string -> Anything that is inside quotes e.g "cat.png"
  • kernel -> NxM matrices, declared with explicit dimensions, e.g. kernel<3,3> k = [ [1,2,3], [4,5,6], [7,8,9] ]
  • image -> Image data types, translated to a struct Image { i32, i32, ptr }

Examples

The examples directory has four small showcase programs, each demonstrating a different style of picceler program:

  • photo_pipeline.pic — chains several image effects together over a real photo and saves the results.
  • blend_and_compare.pic — composites two images together with blend() and diff().
  • language_tour.pic — functions, if/else if/else, for loops, and print() formatting, with no image operations.
  • interactive_dilate.pic — the read_string()/read_number() interactive-CLI pattern.

Run any of them from your build directory (see BUILD.md):

./picceler -o myExecutable ./examples/FILENAME.pic
./myExecutable # Try running it!