> From the perspective where local reasoning is the most desirable property a language can have
That's a perspective. If you're looking for a low-level language, then Scheme isn't it. (Forget iconicity - Scheme is garbage-collected. And supports continuations!)
If you don't program in machine code - which would maximize local reasoning - then you must know the language with the Correct balance of local reasoning and higher-level constructs. Knowing which language that is would add specificity to this discussion...
https://prescheme.org/
Pre-Scheme is a statically typed dialect of the Scheme programming language, combining the flexibility of Scheme with the efficiency and low-level machine access of C. The compiler uses type inference, partial evaluation, and other correctness-preserving transformations to compile a subset of Scheme into C with no additional runtime overhead. This makes Pre-Scheme a viable alternative to C for programming virtual machines, operating systems, and embedded systems where the runtime overhead of a complete Scheme implementation is not desirable.
https://ryansuchocki.github.io/microscheme/
Microscheme, or (ms) for short, is a functional programming language for the Arduino, and for Atmel 8-bit AVR microcontrollers in general. Microscheme is a subset of Scheme, in the sense that every valid (ms) program is also a valid Scheme program (with the exception of Arduino hardware-specific primitives). The (ms) compiler performs function inlining, and features an aggressive tree-shaker, eliminating unused top-level definitions. Microscheme has a robust FFI (Foreign Function Interface) meaning that C code may be invoked directly from (ms) programs. Therefore, the power of the existing wealth of Arduino libraries is available within Microscheme.
Also, CRUNCH from Chicken Scheme:
https://wiki.call-cc.org/eggref/6/crunch
Ah, thank you. It slipped my mind.
CRUNCH is an embedded compiler for a statically typed subset of R7RS Scheme, generating C code. The compiler uses type inference to decorate the code with type information without requiring declarations. CRUNCH can be used to translate embedded Scheme code sections, whole programs or multiple source modules into standalone executables or compiled code that can be invoked from Scheme.
The generated C code uses a small runtime-system contained completely in a single C header file. Reference counting is used for managing aggregate data like strings which removes the need for full tracing garbage collection or manual memory management while still having a relatively small overhead.
Since more or less a direct translation of Scheme to C is done, the generated code should run at roughly the same performance as C. No type-checking takes place as the types of all values have been inferred at compile time, and values are not tagged. With the exception of reference counted objects there is no additional runtime overhead and Scheme and C can directly interchange data. This makes CRUNCH very appropriate for writing programs that need a maximum of speed or that are target for constrained environments like deeply embedded systems. The code is portable to all systems that at least have a C compiler.
UNICODE strings are supported and can optionally be disabled for improving performance and reducing code size.
CRUNCH is heavily inspired by PreScheme, the low-level compiler that is originally part of the Scheme48 project. In fact, CRUNCH can be considered a modern reimplementation of PreScheme written in and for use with CHICKEN.
See also:
https://news.ycombinator.com/item?id=42440767Fair point.
That's not at all what local reasoning means. Local reasoning is the property that a piece of code contains (when including the call graph) everything that can affect what it does. All mutation of a value is kept within some scope of ownership of that value. If you want to understand a piece of code, you can do it by understanding that piece of code, not the program as a whole.
Assembly makes non-local reasoning mandatory, as any code can update any location in memory without restriction. All memory accesses are global. References need not even be by name - they can be via computed addresses. There are no restrictions in place allowing the structure of the program to provide boundaries on what pieces of code may be understood as units.
Homoiconicity is orthogonal to local reasoning. It just means the syntax is represented by the native data format, nested lists. This does make code generation very straightforward with list processing primitives.
Mutation in scheme is possible, via set!, and set-car! and set-cdr!, but it's not recommended. Functional program design side-steps the issue.
see also:
SICP: 3.1.3 The Costs of Introducing Assignment
https://sarabander.github.io/sicp/html/3_002e1.xhtml#g_t3_00...
Yes, I know they're orthogonal. But every example of using macros I see in lisp is either some horrendous non-local logic, or something that can be done better using simpler tools. Where's the killer use case that clearly is good software engineering and not papering over the lack of another feature?