Skip to content

arxlang/arx

Arx

Arx is a new programming language that uses the power of LLVM to provide multi-architecture machine target code generation. Arx aims to provide native list and tensor abstractions with a builtin runtime backed internally by IRx.

Arx is a prototype that should replace the current Arx compiler in c++.

If you want more information about ArxLang, please check the original project in c++: https://github.com/arxlang/arx

Monorepo Workflow

This repository contains the lockstep-released Arx ecosystem packages:

  • packages/astx: astx
  • packages/irx: pyirx
  • packages/arx: arxlang

Root Poetry installs these packages as editable path dependencies for local development. The package sources should still depend on released package names and versions; local checkout wiring belongs in the root pyproject.toml.

Releases are intentionally lockstep. A semantic-release run updates one shared version across the root project and all three package projects, then scripts/build.sh and scripts/publish.sh build and publish astx, pyirx, and arxlang together.

Common local tasks:

makim arx.unittests
makim astx.unittests
makim irx.unittests
makim all.lint
makim docs.build

Link Modes

Arx supports explicit executable link modes:

arx program.x --link-mode auto    # default, use toolchain default
arx program.x --link-mode pie     # force PIE executable
arx program.x --link-mode no-pie  # force non-PIE executable

Troubleshooting (PIE / Colab / Conda)

If you hit an error like:

relocation R_X86_64_32 ... can not be used when making a PIE object

use:

arx program.x --link-mode no-pie

This typically happens on environments where the linker defaults to PIE while objects were not compiled in a PIE-compatible mode.

Testing

Arx now exposes list[...] and tensor[...] as distinct public collection forms:

fn pick(grid: tensor[i32, 2, 2]) -> i32:
  return grid[1, 0]

fn main() -> i32:
  var grid: tensor[i32, 2, 2] = [[1, 2], [3, 4]]
  var ids: list[i32] = [5, 6, 7, 8]
  return pick(grid) + ids[2]

Use:

  • list[T] for generic collection values
  • tensor[T, N] and tensor[T, D0, D1, ..., DN] for fixed-shape tensors
  • tensor[T, ...] for runtime-shaped tensor parameters

Tensor details stay user-facing in terms of element types, shape, dimensions, and indexing. In this phase, variable, field, and return tensor annotations must declare at least one static shape dimension. IRx owns the Arrow C++ backed Tensor runtime and lowering. Current tensor element types are fixed-width numeric types: i8, i16, i32, i64, f32, and f64. For fixed-shape tensors, ... in documentation is descriptive notation for additional integer dimensions, not the runtime-shaped marker. Runtime-shaped tensor parameters can be passed through function boundaries, but indexed access requires static-shape annotations until dynamic tensor indexing lands in IRx.

Arx uses Tensor for homogeneous N-dimensional data and reserves Array for one-dimensional Arrow-style data where the language exposes it. Future heterogeneous dataframe or table support will use a separate surface type.

Arx also ships a bundled pure-Arx standard library under the reserved stdlib namespace:

import math from stdlib

fn main() -> i32:
  return math.square(4)

Compiler-provided builtins stay separate from stdlib. Builtin sources live in packages/arx/src/arx/builtins/*.x, are bundled inside the installed arx package, and are resolved by dedicated compiler logic instead of user-project module lookup. Those bundled builtin modules are internal compiler assets, not a public stdlib-style import namespace. User code does not import builtins; builtin functions such as range(...) are available automatically.

fn main() -> none:
  print(range(0, 4)[2])

The first builtin module is generators. Its current MVP exposes range(start, stop[, step]) -> list[i32], while future overloads and yield-backed generator semantics will grow in the same area. Positive steps count up, negative steps count down, and step == 0 raises an assertion failure. For-in loops can iterate over list-valued expressions such as range(...), list literals, and list variables. Ambient builtin names such as range are injected only when not shadowed, so a local function or import with the same name overrides the builtin in that module.

Arx now supports fatal assertion statements in the language surface:

fn test_add() -> none:
  assert 1 + 1 == 2
  assert 2 + 2 == 4, "2 + 2 should be 4"

You can run compiled tests with the new arx test subcommand:

arx test
arx test packages/arx/tests/arx/test_math.x --list
arx test -k square
arx test -x
arx test --keep-artifacts
arx test --exclude "packages/arx/tests/arx/slow_*.x"

By default the runner searches tests/ for files matching test_*.x, discovers zero-argument test_* functions that return none, and executes each test in its own compiled subprocess. Test identifiers use the cwd-relative path of the source file (without the .x suffix) joined to the function name via ::, for example packages/arx/tests/arx/test_math::test_square, so same-named files in parallel directories stay distinct.

You can override discovery from .arxproject.toml:

[tests]
paths = ["tests", "integration"]
exclude = ["tests/experimental_*.x"]
file_pattern = "test_*.x"
function_pattern = "test_*"

CLI flags always win over [tests] settings. In v1, shared top-level support is intentionally narrow: imports, extern declarations, class declarations, and helper functions are preserved, while module-scope variable declarations and other top-level executable code are not supported yet.