diff --git a/README.md b/README.md index 3f99875..37ea8ef 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,14 @@ pip install frscript - **Command line launcher** (`fr`) - **Compiled language** - Fast by design - **File and Socket I/O** - Low-level file operations and sockets. +- **HTTP library** - A working HTTP/1.1 server/client library in `examples/http/http_lib.fr`, running on all three backends. +- **Fr OS target** - `fr native ... --target fr-os` emits a static Fr OS userspace ELF using the Fr OS syscall ABI and user ISA profile. - **Multiprocessing**. - Easy threading with fork() and wait(). - **Python integration** - You can use any Python libraries with Frscript. -- **Aggressive optimization** - Bytecode-level optimizations. Faster than python. +- **Aggressive optimization** - Purity call-graph analysis, interprocedural ranges, call-site specialization, pure constant-call folding, LICM, SROA/escape analysis, pure-function lookup tables, inline list fast paths and assembly peepholes. +- **Deterministic memory** - Reference-counted native structs/lists/sets with function-scope cleanup, plus SROA and stack allocation for non-escaping values. +- **Modern memory model** - Reference-semantics structs/lists/sets, scope-based reference counting, automatic stack allocation for non-escaping values, and a free-list native allocator. +- **Opt-in pointers** - `int*`, `&value`, `*p`, pointer fields and pointer parameters, lowered directly to addresses in native code. - **Stack-based VM** - Fast and memory efficient. - **Readable bytecode** - Optimize hot code manually - **WASM Backend** - Run sandboxed code in web browsers. diff --git a/TODO.md b/TODO.md index a990377..d80828e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,117 +1,53 @@ - -# TODO: Native no-libc runtime - Remaining Work - -## Current State - -- Native tests passing: 141/247 -- Hello world binary: 222 bytes (no libc) -- Index error tests pass with ?line,0:message format -- Basic arithmetic, function calls, list bounds checking work - -## Remaining Test Failures (106 tests) - -### 1. Assertion Failures (2 tests) - -- `assertions/assert_fail.fr` - Output "" != expected "Assertion failed" -- `assertions/assert_output.fr` - Output "" != expected "hello" -- **Fix**: runtime_assert in fr_rt.c outputs empty instead of message. Need to fix fr_fputs usage. - -### 2. Control Flow - Loops (8 tests) - -- `control_flow/for_in_list.fr` - Output "" != expected "1\n2\n3" -- `control_flow/for_in_string.fr` - in function `_fr_main_fall_1` -- `control_flow/for_in_empty_string.fr` - in function `_fr_main_fall_1` -- `control_flow/for_range.fr` - Output "" != expected "0\n1\n2\n3\n4" -- `control_flow/for_range_start.fr` - Output "" != expected "2\n3\n4" -- `control_flow/for_range_step.fr` - Output "" != expected "0\n2\n4\n6\n8" -- `control_flow/for_range_step_negative.fr` - Output "" != expected "10\n7\n4\n1" -- `control_flow/for_range_step_odd.fr` - Output "" != expected "1\n3\n5\n7\n9" -- `control_flow/break_for.fr` - Output "" != expected "0\n1\n2\n3\n4" -- `control_flow/continue_for.fr` - Output "" != expected "0\n1\n3\n4" -- `control_flow/break_while.fr` - Output "" != expected "0\n1\n2" -- **Fix**: Loop iteration over lists/strings/ranges may be missing runtime support or codegen has issues with loop constructors. - -### 3. Struct Allocation (12 tests) - -- `data_structures/struct_basic.fr` - undefined reference to `_fr_heap_ptr` -- `data_structures/struct_field_assign.fr` -- `data_structures/struct_field_cache.fr` -- `data_structures/struct_get_local.fr` -- `data_structures/struct_mixed_types.fr` -- `data_structures/struct_multi_field_read.fr` -- `data_structures/struct_nested.fr` -- `data_structures/struct_return.fr` -- `data_structures/struct_string_field.fr` -- `data_structures/struct_in_list.fr` -- `data_structures/struct_as_param.fr` -- **Fix**: Codegen emits `_fr_heap_ptr` global but fr_rt.c uses mmap-based heap with `heap_base`/`heap_ptr` locals. Need to either: - - Add `_fr_heap_ptr` global to fr_rt.c and update init_heap, OR - - Change codegen to call `runtime_alloc_struct(size)` instead of direct heap access - -### 4. List Operations (8 tests) - -- `data_types/list_append.fr` - Output "[list]" != expected "[1, 2, 3, 4]" -- `data_types/list_empty.fr` - Output "[list]" != expected "[]" -- `data_types/list_assignment.fr` - Output "" != expected "10" -- `data_types/list_indexing.fr` - Output "" != expected "1" -- `data_types/list_literal.fr` - Output "[list]" != expected "[1, 2, 3]" -- `data_types/list_len.fr` - Output "" != expected "3" -- `data_types/list_method_syntax.fr` - Output "" != expected "5" -- `data_types/list_pop.fr` - in function `_fr_main_main_entry` -- `data_types/list_pop_last.fr` - in function `_fr_main_main_entry` -- `data_types/list_pop_remaining.fr` - in function `_fr_main_main_entry` -- **Fix**: runtime_list_to_str returns "[list]" stub. Need proper list formatting. Also need to verify list append/set operations work. - -### 5. Set Operations (3 tests) - -- `data_types/set_add_duplicate.fr` - Output "{set}" != expected "{1, 2, 3}" -- `data_types/set_empty.fr` - Output "{set}" != expected "{}" -- `data_types/set_string.fr` - Output "{set}" != expected "{hello, world}" -- **Fix**: runtime_set_to_str returns "{set}" stub. Need proper set formatting with sorted elements. - -### 6. String Operations (1 test) - -- `control_flow/switch_string.fr` - in function `_fr_main_main_entry` -- **Fix**: Missing runtime_str_to_lower or similar string case conversion for switch matching. - -### 7. Float Formatting (3 tests - already fixed, need verification) - -- `data_types/float_multiplication.fr` - Output "7.50" != expected "7.5" -- `data_types/float_division.fr` - Output "3.50" != expected "3.5" -- `data_types/float_negative.fr` - Output "-3.50" != expected "-3.5" -- `data_types/float_addition.fr` - Output "3.50" != expected "3.5" -- **Fix**: Already modified runtime_float_to_str to strip trailing zeros. Need to re-test. - -### 8. Misc (1 test) - -- `misc/wait_basic.fr` - in function `_fr_main_main_entry` -- **Fix**: Missing fork/wait syscalls or runtime support. - -### 9. Input Function - -- Need to implement runtime_input_str or similar if any tests use input() - -## Implementation Order - -1. Fix struct allocation (_fr_heap_ptr issue) - unblocks 12 tests -2. Fix list_to_str - unblocks 8 tests -3. Fix set_to_str - unblocks 3 tests -4. Fix assert output - unblocks 2 tests -5. Fix loop iteration - unblocks 10 tests -6. Fix float formatting verify - 3-4 tests -7. Fix switch_string - 1 test -8. Fix wait_basic - 1 test -9. Fix any remaining individual tests - -## Files to Modify - -- `runtime/fr_rt.c` - Add missing runtime functions -- `src/optimizer/codegen.py` - Fix struct alloc to use runtime call or add _fr_heap_ptr -- `src/run_single_test.py` - Already updated, may need minor fixes -- `src/cli.py` - Already updated +# Roadmap + +## Completed + +- Native no-libc runtime: **254/254 native tests pass**. +- Python VM: **325/325 tests pass**. +- C VM: **325/325 tests pass**. +- Freestanding native runtime: + - real mmap-backed allocator with segregated free lists, `free`, `realloc` + - `printf` implementation (`%d`, `%ld`, `%s`, `%f`, `%g`, `%x`, `%c`) + - lists/sets/strings/math/file I/O/process/sleep/input + - `_setjmp`/`longjmp` based exception handling +- Memory model: + - reference semantics for structs, lists and sets in all three runtimes + - C VM reference counting for mutable aggregates + - Python VM garbage collection + - native SROA + escape analysis + stack allocation for non-escaping structs +- Language-level pointers: + - `int*`, `*int`, `int**` types and struct pointer fields + - `&lvalue`, `*p`, `*p = value`, `**pp = value` + - pointers to locals, globals, struct fields and list elements + - automatic dereference for `p.field` and chained `p.next.value` + - global variables and pointers to globals in all runtimes +- Compiler analysis: + - call-graph side-effect/purity analysis + - interprocedural integer/boolean range analysis + - natural-loop induction-variable bounds + - branch-sensitive per-call-site specialization with constants and ranges + - dead branch / unreachable block removal + - linear block merging and dead function elimination + - purity-aware DCE/CSE + - compile-time pure-function simulation with embedded lookup tables + - tunable budgets for every speculative analysis (`OptimizerConfig`) +- Native reference counting: + - function allocation scopes + - retain on global/list/field/pointer/return escapes + - release on overwrite and scope exit + - safe stack/string pointer handling via object magic/range checks + +## Next + +- Collect reference-count cycles (managed object cycles are currently leaked, + like most RC systems). +- Pointer lifetimes beyond a frame could become a compile-time error rather than + a documented C-style rule. +- Wasm backend needs the new pointer/global opcodes; it is currently not part of + the validated test matrix. ## Notes -- Wasm backend has 208 failures but is not the current focus -- C VM has 11 failures (separate from native) -- Python VM has 2 failures (separate from native) +- `int *args` is still varargs; use `int* p` or `*int p` for pointer params. +- Pointers into a list element are invalidated when the list grows. +- Escaping native heap objects live until process exit. diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..3eb848a --- /dev/null +++ b/bench/README.md @@ -0,0 +1,46 @@ +# Benchmarks + +The benchmarks are ordinary Fr programs and are not part of the test suite. + +## Vector reductions + +`vector_bench.fr` builds 100,000-element integer and float lists and runs +integer sum, float sum, and float sum-of-squares reductions. The native backend +recognizes these loops and emits AVX2 vector loops with a scalar remainder, +while the Python/C VMs run the same source as scalar loops. + +```sh +python3 src/cli.py compile bench/vector_bench.fr -o /tmp/vec.bc +python3 src/cli.py native /tmp/vec.bc -o /tmp/vec -O3 +time /tmp/vec +``` + +## N-body + +`nbody.fr` is the correctness-sized run (1,000 steps). `nbody_bench.fr` builds +128 bodies and runs 1,000 steps; it combines nested loops, struct field traffic, +list indexing/set, floating-point math, calls and reference-counted heap +objects. `memory_bench.fr` holds 250,000 reference-counted structs in a list and +walks them, giving a reproducible allocation/RSS measurement. + +```sh +python3 src/cli.py compile bench/nbody_bench.fr -o /tmp/nbody.bc +python3 src/cli.py native /tmp/nbody.bc -o /tmp/nbody -O3 +time /tmp/nbody +/usr/bin/time -f 'rss_kb=%M elapsed=%e' /tmp/nbody +``` + +Memory benchmark: + +```sh +python3 src/cli.py compile bench/memory_bench.fr -o /tmp/mem.bc +python3 src/cli.py native /tmp/mem.bc -o /tmp/mem -O3 +/usr/bin/time -f 'rss_kb=%M elapsed=%e' /tmp/mem +``` + +The compiler optimizations validated against this target include scope elision, +LICM, compare/branch fusion, LEA instruction selection, inline list length/list +get fast paths, XMM liveness preservation across calls, pure constant-call +folding, and assembly peepholes. The full test suite (which exercises unrelated +integer, string, struct, list, error and HTTP workloads) is run after every +optimization change. diff --git a/bench/memory_bench.fr b/bench/memory_bench.fr new file mode 100644 index 0000000..04588d0 --- /dev/null +++ b/bench/memory_bench.fr @@ -0,0 +1,26 @@ +// Memory/allocation benchmark: hold 250k reference-counted structs in a list. + +struct Node { + int a + int b + int c + int d +} + +void main() { + list nodes = [] + int i = 0 + while (i < 250000) { + nodes.append(Node(i, i + 1, i + 2, i + 3)) + i = i + 1 + } + + int total = 0 + int j = 0 + while (j < len(nodes)) { + Node n = nodes[j] + total = total + n.a + n.d + j = j + 1 + } + println(str(total)) +} diff --git a/bench/nbody.fr b/bench/nbody.fr new file mode 100644 index 0000000..e45632d --- /dev/null +++ b/bench/nbody.fr @@ -0,0 +1,81 @@ +struct Body { + float x + float y + float z + float vx + float vy + float vz + float mass +} + +float energy(list bodies) { + float e = 0.0 + int i = 0 + while (i < len(bodies)) { + Body a = bodies[i] + e = e + 0.5 * a.mass * (a.vx * a.vx + a.vy * a.vy + a.vz * a.vz) + int j = i + 1 + while (j < len(bodies)) { + Body b = bodies[j] + float dx = a.x - b.x + float dy = a.y - b.y + float dz = a.z - b.z + float d = sqrt(dx * dx + dy * dy + dz * dz) + e = e - (a.mass * b.mass) / d + j = j + 1 + } + i = i + 1 + } + return e +} + +void advance(list bodies, float dt, int steps) { + int s = 0 + while (s < steps) { + int i = 0 + while (i < len(bodies)) { + Body a = bodies[i] + int j = i + 1 + while (j < len(bodies)) { + Body b = bodies[j] + float dx = a.x - b.x + float dy = a.y - b.y + float dz = a.z - b.z + float d2 = dx * dx + dy * dy + dz * dz + 0.01 + float inv = 1.0 / (d2 * sqrt(d2)) + a.vx = a.vx - dx * b.mass * inv + a.vy = a.vy - dy * b.mass * inv + a.vz = a.vz - dz * b.mass * inv + b.vx = b.vx + dx * a.mass * inv + b.vy = b.vy + dy * a.mass * inv + b.vz = b.vz + dz * a.mass * inv + bodies[j] = b + j = j + 1 + } + bodies[i] = a + i = i + 1 + } + int k = 0 + while (k < len(bodies)) { + Body p = bodies[k] + p.x = p.x + dt * p.vx + p.y = p.y + dt * p.vy + p.z = p.z + dt * p.vz + bodies[k] = p + k = k + 1 + } + s = s + 1 + } +} + +void main() { + list bodies = [ + Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 39.47), + Body(4.84, -1.16, -0.10, 0.60, 2.81, -0.02, 0.037), + Body(8.34, 4.12, -0.40, -1.01, 1.85, 0.01, 0.011), + Body(12.89, -15.10, -0.20, 1.08, 0.44, -0.01, 0.001), + Body(15.37, -25.91, 0.10, 0.97, 0.58, 0.00, 0.0005) + ] + advance(bodies, 0.01, 1000) + println(str(energy(bodies))) +} diff --git a/bench/nbody_bench.fr b/bench/nbody_bench.fr new file mode 100644 index 0000000..1e7b9ba --- /dev/null +++ b/bench/nbody_bench.fr @@ -0,0 +1,93 @@ +// N-body benchmark with a larger body count to exercise list/struct memory. +// Generate 128 bodies, then run 1,000 simulation steps. + +struct Body { + float x + float y + float z + float vx + float vy + float vz + float mass +} + +float energy(list bodies) { + float e = 0.0 + int i = 0 + while (i < len(bodies)) { + Body a = bodies[i] + e = e + 0.5 * a.mass * (a.vx * a.vx + a.vy * a.vy + a.vz * a.vz) + int j = i + 1 + while (j < len(bodies)) { + Body b = bodies[j] + float dx = a.x - b.x + float dy = a.y - b.y + float dz = a.z - b.z + float d = sqrt(dx * dx + dy * dy + dz * dz) + e = e - (a.mass * b.mass) / d + j = j + 1 + } + i = i + 1 + } + return e +} + +void advance(list bodies, float dt, int steps) { + int s = 0 + while (s < steps) { + int i = 0 + while (i < len(bodies)) { + Body a = bodies[i] + int j = i + 1 + while (j < len(bodies)) { + Body b = bodies[j] + float dx = a.x - b.x + float dy = a.y - b.y + float dz = a.z - b.z + float d2 = dx * dx + dy * dy + dz * dz + 0.01 + float inv = 1.0 / (d2 * sqrt(d2)) + a.vx = a.vx - dx * b.mass * inv + a.vy = a.vy - dy * b.mass * inv + a.vz = a.vz - dz * b.mass * inv + b.vx = b.vx + dx * a.mass * inv + b.vy = b.vy + dy * a.mass * inv + b.vz = b.vz + dz * a.mass * inv + bodies[j] = b + j = j + 1 + } + bodies[i] = a + i = i + 1 + } + int k = 0 + while (k < len(bodies)) { + Body p = bodies[k] + p.x = p.x + dt * p.vx + p.y = p.y + dt * p.vy + p.z = p.z + dt * p.vz + bodies[k] = p + k = k + 1 + } + s = s + 1 + } +} + +void main() { + list bodies = [] + int i = 0 + while (i < 128) { + float fi = float(i) + float mass = 0.001 + 0.01 / (fi + 1.0) + bodies.append(Body( + fi * 0.7, + -fi * 0.31, + float(i % 7) * 0.13, + float(i % 5) * 0.02, + float(i % 11) * 0.015, + float(i % 3) * 0.01, + mass + )) + i = i + 1 + } + advance(bodies, 0.001, 1000) + println(str(energy(bodies))) +} diff --git a/bench/vector_bench.fr b/bench/vector_bench.fr new file mode 100644 index 0000000..15272a4 --- /dev/null +++ b/bench/vector_bench.fr @@ -0,0 +1,44 @@ +int sum_i(list xs) { + int total = 0 + int i = 0 + while (i < len(xs)) { + total = total + xs[i] + i = i + 1 + } + return total +} + +float sum_f(list xs) { + float total = 0.0 + int i = 0 + while (i < len(xs)) { + total = total + xs[i] + i = i + 1 + } + return total +} + +float sumsq_f(list xs) { + float total = 0.0 + int i = 0 + while (i < len(xs)) { + float v = xs[i] + total = total + v * v + i = i + 1 + } + return total +} + +void main() { + list ints = [] + list floats = [] + int i = 0 + while (i < 100000) { + ints.append(i) + floats.append(float(i) * 0.5) + i = i + 1 + } + println(str(sum_i(ints))) + println(str(sum_f(floats))) + println(str(sumsq_f(floats))) +} diff --git a/cases/config.json b/cases/config.json index 369afba..a1ff16e 100644 --- a/cases/config.json +++ b/cases/config.json @@ -1,51 +1 @@ -{ - "native": { - "enabled": true, - "timeout": 2, - "ignore": [ - "syntax_errors", - "python_interop", - "imports/c_import.fr", - "io/socket*", - "misc/goto_with_return.fr", - "runtime/bigint_stress.fr", - "data_structures/static_list_basic.fr" - ], - "skip_mismatch": [], - "allow_timeout_failures": false - }, - "c": { - "enabled": true, - "timeout": 2, - "ignore": [ - "syntax_errors", - "imports/c_import.fr" - ], - "skip_mismatch": [], - "allow_timeout_failures": false - }, - "py": { - "enabled": true, - "timeout": 2, - "ignore": [ - "imports/c_import.fr", - "misc/goto_with_return.fr" - ], - "skip_mismatch": [], - "allow_timeout_failures": false - }, - "wasm": { - "enabled": true, - "timeout": 2, - "ignore": [ - "syntax_errors", - "python_interop", - "io/file*", - "io/socket*", - "imports/c_import.fr", - "misc/goto_with_return.fr" - ], - "skip_mismatch": [], - "allow_timeout_failures": false - } -} \ No newline at end of file +{"native":{"enabled":true,"timeout":2,"ignore":["syntax_errors","python_interop","imports/c_import.fr","misc/goto_with_return.fr"],"skip_mismatch":[],"allow_timeout_failures":false},"c":{"enabled":true,"timeout":2,"ignore":["syntax_errors","imports/c_import.fr","*deep_scope_overflow*"],"skip_mismatch":[],"allow_timeout_failures":false},"py":{"enabled":true,"timeout":2,"ignore":["imports/c_import.fr","misc/goto_with_return.fr","*deep_scope_overflow*"],"skip_mismatch":[],"allow_timeout_failures":false},"wasm":{"enabled":true,"timeout":2,"ignore":["syntax_errors","python_interop","io/file*","io/socket*","imports/c_import.fr","misc/goto_with_return.fr"],"skip_mismatch":[],"allow_timeout_failures":false}} diff --git a/cases/data_structures/struct_reference_semantics.fr b/cases/data_structures/struct_reference_semantics.fr new file mode 100644 index 0000000..3ebd6ee --- /dev/null +++ b/cases/data_structures/struct_reference_semantics.fr @@ -0,0 +1,18 @@ +// !9 +// ![9, 2] + +struct Point { + int x +} + +void main() { + Point a = Point(3) + Point b = a + b.x = 9 + println(a.x) + + list xs = [1, 2] + list ys = xs + ys[0] = 9 + println(xs) +} diff --git a/cases/data_types/dict_basic.fr b/cases/data_types/dict_basic.fr new file mode 100644 index 0000000..cfb48b8 --- /dev/null +++ b/cases/data_types/dict_basic.fr @@ -0,0 +1,14 @@ +// !1 +// !2 +// !true +// !false + +void main() { + dict d = {} + d["a"] = 1 + d["b"] = 2 + println(d["a"]) + println(d["b"]) + println("a" in d) + println("z" in d) +} diff --git a/cases/data_types/dict_large_int_keys.fr b/cases/data_types/dict_large_int_keys.fr new file mode 100644 index 0000000..103608a --- /dev/null +++ b/cases/data_types/dict_large_int_keys.fr @@ -0,0 +1,12 @@ +// !1 +// !true +// !1 + +void main() { + dict d = {} + int key = 1048577 + d[key] = 1 + println(str(d[key])) + println(key in d) + println(str(d[1048577])) +} diff --git a/cases/data_types/set_large_int.fr b/cases/data_types/set_large_int.fr new file mode 100644 index 0000000..023f639 --- /dev/null +++ b/cases/data_types/set_large_int.fr @@ -0,0 +1,8 @@ +// !true +// !false + +void main() { + set s = {1048577, 2} + println(1048577 in s) + println(1048576 in s) +} diff --git a/cases/error_handling/list_error_preserves_live.fr b/cases/error_handling/list_error_preserves_live.fr new file mode 100644 index 0000000..cdfc080 --- /dev/null +++ b/cases/error_handling/list_error_preserves_live.fr @@ -0,0 +1,13 @@ +// !1.5 + +int main() { + float x = 1.5 + list values = [1] + try { + int y = values[5] + println(str(y)) + } except "IndexError" { + println(str(x)) + } + return 0 +} diff --git a/cases/error_handling/long_raise_message.fr b/cases/error_handling/long_raise_message.fr new file mode 100644 index 0000000..ed9c28f --- /dev/null +++ b/cases/error_handling/long_raise_message.fr @@ -0,0 +1,10 @@ +// !caught + +void main() { + str message = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + try { + raise "RuntimeError" message + } except "RuntimeError" { + println("caught") + } +} diff --git a/cases/functions/local_shadow_global.fr b/cases/functions/local_shadow_global.fr new file mode 100644 index 0000000..c73402f --- /dev/null +++ b/cases/functions/local_shadow_global.fr @@ -0,0 +1,11 @@ +// !2 +// !1 +int x = 1 +void f() { + int x = 2 + println(str(x)) +} +void main() { + f() + println(str(x)) +} diff --git a/cases/http/http_parse_request.fr b/cases/http/http_parse_request.fr new file mode 100644 index 0000000..607d937 --- /dev/null +++ b/cases/http/http_parse_request.fr @@ -0,0 +1,17 @@ +// !POST +// !/echo +// !hello +// !5 +// !tester + +import ../../examples/http/http_lib + +void main() { + str raw = "POST /echo HTTP/1.1\r\nHost: example\r\nContent-Length: 5\r\nUser-Agent: tester\r\n\r\nhello" + HttpRequest req = http_parse_request(raw) + println(req.method) + println(req.path) + println(req.body) + println(http_get_header(req.headers, "content-length")) + println(http_get_header(req.headers, "user-agent")) +} diff --git a/cases/http/http_parse_response.fr b/cases/http/http_parse_response.fr new file mode 100644 index 0000000..be030a1 --- /dev/null +++ b/cases/http/http_parse_response.fr @@ -0,0 +1,13 @@ +// !200 +// !abc +// !3 + +import ../../examples/http/http_lib + +void main() { + str raw = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\n\r\nabc" + HttpResponse res = http_parse_response(raw) + println(str(res.status)) + println(res.body) + println(http_get_header(res.headers, "content-length")) +} diff --git a/cases/http/http_serialize_response.fr b/cases/http/http_serialize_response.fr new file mode 100644 index 0000000..75404f3 --- /dev/null +++ b/cases/http/http_serialize_response.fr @@ -0,0 +1,14 @@ +// !true +// !true +// !false + +import ../../examples/http/http_lib + +void main() { + HttpResponse res = HttpResponse(200, "hello", []) + res.headers.append("Content-Type: text/plain") + str raw = http_serialize_response(res) + println(contains(raw, "Content-Length: 5")) + println(contains(raw, "Connection: close")) + println(contains(raw, "X-Missing: yes")) +} diff --git a/cases/io/filesystem_basic.fr b/cases/io/filesystem_basic.fr new file mode 100644 index 0000000..84562d6 --- /dev/null +++ b/cases/io/filesystem_basic.fr @@ -0,0 +1,33 @@ +// !true +// !4 +// !true +// !true +// !moved.txt +// !false +// !false + +void main() { + str base = "/tmp/fr_cov_" + str(getpid()) + str nested = pathjoin([base, "a", "b"]) + makedirs(nested) + println(isdir(nested)) + + str file = pathjoin([base, "data.txt"]) + int handle = fopen(file, "w") + fwrite(handle, "test") + fclose(handle) + println(str(getsize(file))) + println(contains(listdir(base), "data.txt")) + + str moved = pathjoin([base, "moved.txt"]) + rename(file, moved) + println(exists(moved)) + println(basename(moved)) + println(dirname(moved) != base) + + remove(moved) + rmdir(nested) + rmdir(pathjoin([base, "a"])) + rmdir(base) + println(exists(base)) +} diff --git a/cases/io/listdir_refcount.fr b/cases/io/listdir_refcount.fr new file mode 100644 index 0000000..1b9c08c --- /dev/null +++ b/cases/io/listdir_refcount.fr @@ -0,0 +1,6 @@ +// !true + +void main() { + list entries = listdir("/tmp") + println(len(entries) > 0) +} diff --git a/cases/io/print_no_newline.fr b/cases/io/print_no_newline.fr new file mode 100644 index 0000000..614fd5d --- /dev/null +++ b/cases/io/print_no_newline.fr @@ -0,0 +1,6 @@ +// !AB + +void main() { + print("A") + print("B") +} diff --git a/cases/math/round_half_away.fr b/cases/math/round_half_away.fr new file mode 100644 index 0000000..a16ee64 --- /dev/null +++ b/cases/math/round_half_away.fr @@ -0,0 +1,9 @@ +// !3 +// !-3 + +void main() { + int a = round(2.5) + int b = round(-2.5) + println(str(a)) + println(str(b)) +} diff --git a/cases/math/trig_identities.fr b/cases/math/trig_identities.fr new file mode 100644 index 0000000..d1d56c8 --- /dev/null +++ b/cases/math/trig_identities.fr @@ -0,0 +1,9 @@ +// !true +// !true +// !true + +void main() { + println(sin(0.0) == 0.0) + println(cos(0.0) == 1.0) + println(tan(0.0) == 0.0) +} diff --git a/cases/math/trig_int_arg.fr b/cases/math/trig_int_arg.fr new file mode 100644 index 0000000..abf5338 --- /dev/null +++ b/cases/math/trig_int_arg.fr @@ -0,0 +1,9 @@ +// !true +// !true +// !true + +void main() { + println(sin(1) > 0.8) + println(cos(1) > 0.5) + println(tan(1) > 1.5) +} diff --git a/cases/memory/arg_escape_store.fr b/cases/memory/arg_escape_store.fr new file mode 100644 index 0000000..4c751f4 --- /dev/null +++ b/cases/memory/arg_escape_store.fr @@ -0,0 +1,21 @@ +// !7 + +struct P { + int x +} + +list saved = [] + +void keep(P p) { + saved.append(p) +} + +void make() { + P local = P(7) + keep(local) +} + +void main() { + make() + println(str(saved[0].x)) +} diff --git a/cases/memory/deep_scope_overflow.fr b/cases/memory/deep_scope_overflow.fr new file mode 100644 index 0000000..9a1fdd1 --- /dev/null +++ b/cases/memory/deep_scope_overflow.fr @@ -0,0 +1,14 @@ +#pragma no_eval +// !1050 + +int depth(int n) { + list keep_scope = [] + if (n == 0) { + return 0 + } + return depth(n - 1) + 1 +} + +void main() { + println(str(depth(1050))) +} diff --git a/cases/memory/global_pointer.fr b/cases/memory/global_pointer.fr new file mode 100644 index 0000000..8fa7b89 --- /dev/null +++ b/cases/memory/global_pointer.fr @@ -0,0 +1,25 @@ +// !9 +// !10 +// !20 + +int counter = 7 + +struct Box { + int value +} + +Box box = Box(1) + +void update(int v) { + counter = v + box.value = v + 1 +} + +void main() { + update(9) + println(counter) + println(box.value) + int* p = &counter + *p = 20 + println(counter) +} diff --git a/cases/memory/global_pointer_float.fr b/cases/memory/global_pointer_float.fr new file mode 100644 index 0000000..792b84c --- /dev/null +++ b/cases/memory/global_pointer_float.fr @@ -0,0 +1,9 @@ +// !4.5 + +float value = 3.5 + +void main() { + float* p = &value + *p = 4.5 + println(str(value)) +} diff --git a/cases/memory/list_set_managed_refs.fr b/cases/memory/list_set_managed_refs.fr new file mode 100644 index 0000000..44d6aa6 --- /dev/null +++ b/cases/memory/list_set_managed_refs.fr @@ -0,0 +1,20 @@ +// !2 +// !3 + +struct P { + int x +} + +P make(int v) { + return P(v) +} + +void main() { + list values = [make(1)] + P a = make(2) + values[0] = a + println(str(values[0].x)) + list other = [make(3)] + values[0] = other[0] + println(str(values[0].x)) +} diff --git a/cases/memory/pointer_basic.fr b/cases/memory/pointer_basic.fr new file mode 100644 index 0000000..3e2d7ce --- /dev/null +++ b/cases/memory/pointer_basic.fr @@ -0,0 +1,13 @@ +// !9 +// !9 +// !10 + +void main() { + int x = 5 + int* p = &x + *p = 9 + println(*p) + println(x) + *p = *p + 1 + println(x) +} diff --git a/cases/memory/pointer_boxed_assignment.fr b/cases/memory/pointer_boxed_assignment.fr new file mode 100644 index 0000000..88855d5 --- /dev/null +++ b/cases/memory/pointer_boxed_assignment.fr @@ -0,0 +1,11 @@ +// !8 +// !9 + +void main() { + int value = 7 + int* p = &value + value = value + 1 + println(str(*p)) + value = value + 1 + println(str(*p)) +} diff --git a/cases/memory/pointer_conditional_addr.fr b/cases/memory/pointer_conditional_addr.fr new file mode 100644 index 0000000..641f4cb --- /dev/null +++ b/cases/memory/pointer_conditional_addr.fr @@ -0,0 +1,11 @@ +// !7 + +int value = 7 + +void main() { + int* p = 0 + if (getpid() > 0) { + p = &value + } + println(str(*p)) +} diff --git a/cases/memory/pointer_double.fr b/cases/memory/pointer_double.fr new file mode 100644 index 0000000..343ed4e --- /dev/null +++ b/cases/memory/pointer_double.fr @@ -0,0 +1,13 @@ +// !7 +// !11 +// !11 + +void main() { + int x = 7 + int* p = &x + int** pp = &p + println(**pp) + **pp = 11 + println(x) + println(*p) +} diff --git a/cases/memory/pointer_field_assign_reuse.fr b/cases/memory/pointer_field_assign_reuse.fr new file mode 100644 index 0000000..d00de53 --- /dev/null +++ b/cases/memory/pointer_field_assign_reuse.fr @@ -0,0 +1,15 @@ +// !2 +// !3 + +struct P { + int x +} + +void main() { + P p = P(1) + P* q = &p + q.x = 2 + println(str(q.x)) + q.x = 3 + println(str(p.x)) +} diff --git a/cases/memory/pointer_linked_list.fr b/cases/memory/pointer_linked_list.fr new file mode 100644 index 0000000..90f39bb --- /dev/null +++ b/cases/memory/pointer_linked_list.fr @@ -0,0 +1,19 @@ +// !1 +// !2 +// !7 + +struct Node { + int value + Node* next +} + +void main() { + Node a = Node(1, 0) + Node b = Node(2, 0) + a.next = &b + Node* p = &a + println(p.value) + println(p.next.value) + p.next.value = 7 + println(b.value) +} diff --git a/cases/memory/pointer_list_elem.fr b/cases/memory/pointer_list_elem.fr new file mode 100644 index 0000000..ce75acf --- /dev/null +++ b/cases/memory/pointer_list_elem.fr @@ -0,0 +1,10 @@ +// ![1, 99, 3] +// !99 + +void main() { + list nums = [1, 2, 3] + int* e = &nums[1] + *e = 99 + println(nums) + println(*e) +} diff --git a/cases/memory/pointer_param.fr b/cases/memory/pointer_param.fr new file mode 100644 index 0000000..eaedd19 --- /dev/null +++ b/cases/memory/pointer_param.fr @@ -0,0 +1,14 @@ +// !15 +// !30 + +void add_to(int* p, int amount) { + *p = *p + amount +} + +void main() { + int x = 10 + add_to(&x, 5) + println(x) + add_to(&x, x) + println(x) +} diff --git a/cases/memory/pointer_return.fr b/cases/memory/pointer_return.fr new file mode 100644 index 0000000..f2bf7dc --- /dev/null +++ b/cases/memory/pointer_return.fr @@ -0,0 +1,18 @@ +// !5 +// !2 + +int* pick(int* a, int* b, bool first) { + if (first) { + return a + } + return b +} + +void main() { + int x = 1 + int y = 2 + int* p = pick(&x, &y, true) + *p = 5 + println(x) + println(y) +} diff --git a/cases/memory/pointer_struct.fr b/cases/memory/pointer_struct.fr new file mode 100644 index 0000000..f879f08 --- /dev/null +++ b/cases/memory/pointer_struct.fr @@ -0,0 +1,23 @@ +// !9 +// !4 +// !10 + +struct Point { + int x + int y +} + +void bump(Point* p) { + (*p).x = (*p).x + 1 +} + +void main() { + Point p = Point(3, 4) + Point* q = &p + (*q).x = 8 + bump(q) + println(p.x) + println(p.y) + q.y = 10 + println(p.y) +} diff --git a/cases/memory/refcount_ownership.fr b/cases/memory/refcount_ownership.fr new file mode 100644 index 0000000..5378e27 --- /dev/null +++ b/cases/memory/refcount_ownership.fr @@ -0,0 +1,32 @@ +// !4 +// !9 + +struct Point { + int x +} + +struct Box { + Point inner +} + +list make_points(int n) { + list xs = [] + int i = 0 + while (i < n) { + xs.append(Point(i)) + i = i + 1 + } + return xs +} + +Box make_box(int v) { + Point p = Point(v) + return Box(p) +} + +void main() { + list xs = make_points(5) + println(xs[4].x) + Box b = make_box(9) + println(b.inner.x) +} diff --git a/cases/operators/string_equality.fr b/cases/operators/string_equality.fr new file mode 100644 index 0000000..0a78131 --- /dev/null +++ b/cases/operators/string_equality.fr @@ -0,0 +1,12 @@ +// !false +// !true +// !true +// !false +void main() { + str a = "hello" + str b = "world" + println(a == b) + println(a != b) + println(a == "hello") + println(a != "hello") +} diff --git a/cases/optimizer/branch_specialization.fr b/cases/optimizer/branch_specialization.fr new file mode 100644 index 0000000..33f8611 --- /dev/null +++ b/cases/optimizer/branch_specialization.fr @@ -0,0 +1,21 @@ +// !0 +// !2 +// !4 + +#pragma no_eval + +int safe_double(int n) { + if (n > 1000) { + println("unreachable side effect") + return 0 + } + return n * 2 +} + +void main() { + int i = 0 + while (i < 3) { + println(safe_double(i)) + i = i + 1 + } +} diff --git a/cases/optimizer/div_precision.fr b/cases/optimizer/div_precision.fr new file mode 100644 index 0000000..91e13e3 --- /dev/null +++ b/cases/optimizer/div_precision.fr @@ -0,0 +1,11 @@ +// !3002399751580331 + +#pragma no_eval + +int divide(int a, int b) { + return a / b +} + +void main() { + println(str(int(divide(9007199254740993, 3)))) +} diff --git a/cases/optimizer/lookup_table_pure.fr b/cases/optimizer/lookup_table_pure.fr new file mode 100644 index 0000000..501b7f0 --- /dev/null +++ b/cases/optimizer/lookup_table_pure.fr @@ -0,0 +1,30 @@ +// !0 +// !1 +// !3 +// !6 +// !10 +// !15 +// !21 +// !28 +// !36 +// !45 + +#pragma no_eval + +int sum_to(int n) { + int total = 0 + int i = 0 + while (i <= n) { + total = total + i + i = i + 1 + } + return total +} + +void main() { + int k = 0 + while (k < 10) { + println(sum_to(k)) + k = k + 1 + } +} diff --git a/cases/optimizer/nbody_small.fr b/cases/optimizer/nbody_small.fr new file mode 100644 index 0000000..b2424be --- /dev/null +++ b/cases/optimizer/nbody_small.fr @@ -0,0 +1,83 @@ +// !true + +struct Body { + float x + float y + float z + float vx + float vy + float vz + float mass +} + +float energy(list bodies) { + float e = 0.0 + int i = 0 + while (i < len(bodies)) { + Body a = bodies[i] + e = e + 0.5 * a.mass * (a.vx * a.vx + a.vy * a.vy + a.vz * a.vz) + int j = i + 1 + while (j < len(bodies)) { + Body b = bodies[j] + float dx = a.x - b.x + float dy = a.y - b.y + float dz = a.z - b.z + float d = sqrt(dx * dx + dy * dy + dz * dz) + e = e - (a.mass * b.mass) / d + j = j + 1 + } + i = i + 1 + } + return e +} + +void advance(list bodies, float dt, int steps) { + int s = 0 + while (s < steps) { + int i = 0 + while (i < len(bodies)) { + Body a = bodies[i] + int j = i + 1 + while (j < len(bodies)) { + Body b = bodies[j] + float dx = a.x - b.x + float dy = a.y - b.y + float dz = a.z - b.z + float d2 = dx * dx + dy * dy + dz * dz + 0.01 + float inv = 1.0 / (d2 * sqrt(d2)) + a.vx = a.vx - dx * b.mass * inv + a.vy = a.vy - dy * b.mass * inv + a.vz = a.vz - dz * b.mass * inv + b.vx = b.vx + dx * a.mass * inv + b.vy = b.vy + dy * a.mass * inv + b.vz = b.vz + dz * a.mass * inv + bodies[j] = b + j = j + 1 + } + bodies[i] = a + i = i + 1 + } + int k = 0 + while (k < len(bodies)) { + Body p = bodies[k] + p.x = p.x + dt * p.vx + p.y = p.y + dt * p.vy + p.z = p.z + dt * p.vz + bodies[k] = p + k = k + 1 + } + s = s + 1 + } +} + +void main() { + list bodies = [ + Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 39.47), + Body(4.84, -1.16, -0.10, 0.60, 2.81, -0.02, 0.037), + Body(8.34, 4.12, -0.40, -1.01, 1.85, 0.01, 0.011), + Body(12.89, -15.10, -0.20, 1.08, 0.44, -0.01, 0.001), + Body(15.37, -25.91, 0.10, 0.97, 0.58, 0.00, 0.0005) + ] + advance(bodies, 0.01, 10) + println(energy(bodies) > 0.0) +} diff --git a/cases/optimizer/range_unknown_call.fr b/cases/optimizer/range_unknown_call.fr new file mode 100644 index 0000000..bba5aac --- /dev/null +++ b/cases/optimizer/range_unknown_call.fr @@ -0,0 +1,17 @@ +// !1 +// !2 + +#pragma no_eval + +int classify(int n) { + if (n > 10) { + return 1 + } + return 2 +} + +void main() { + println(str(classify(20))) + int n = getpid() % 5 + 1 + println(str(classify(n))) +} diff --git a/cases/optimizer/vector_reduce.fr b/cases/optimizer/vector_reduce.fr new file mode 100644 index 0000000..406e066 --- /dev/null +++ b/cases/optimizer/vector_reduce.fr @@ -0,0 +1,57 @@ +// !0 +// !15 +// !0.0 +// !15.0 +// !55.0 +// !1 +// !6 +// !6.0 + +int sum_i(list xs) { + int total = 0 + int i = 0 + while (i < len(xs)) { + total = total + xs[i] + i = i + 1 + } + return total +} + +float sum_f(list xs) { + float total = 0.0 + int i = 0 + while (i < len(xs)) { + total = total + xs[i] + i = i + 1 + } + return total +} + +float sumsq_f(list xs) { + float total = 0.0 + int i = 0 + while (i < len(xs)) { + float v = xs[i] + total = total + v * v + i = i + 1 + } + return total +} + +void main() { + list empty = [] + list ints = [1, 2, 3, 4, 5] + list floats = [1.0, 2.0, 3.0, 4.0, 5.0] + list single = [1] + list odd = [1, 2, 3] + list odd_f = [1.0, 2.0, 3.0] + + println(str(sum_i(empty))) + println(str(sum_i(ints))) + println(str(sum_f(empty))) + println(str(sum_f(floats))) + println(str(sumsq_f(floats))) + println(str(sum_i(single))) + println(str(sum_i(odd))) + println(str(sum_f(odd_f))) +} diff --git a/cases/optimizer/vector_start_offset.fr b/cases/optimizer/vector_start_offset.fr new file mode 100644 index 0000000..dd1f123 --- /dev/null +++ b/cases/optimizer/vector_start_offset.fr @@ -0,0 +1,16 @@ +// !7 + +int sum_from(list xs, int start) { + int total = 0 + int i = start + while (i < len(xs)) { + total = total + xs[i] + i = i + 1 + } + return total +} + +void main() { + list xs = [1, 2, 3, 4] + println(str(sum_from(xs, 2))) +} diff --git a/doc/fr-os.md b/doc/fr-os.md new file mode 100644 index 0000000..4d7cf91 --- /dev/null +++ b/doc/fr-os.md @@ -0,0 +1,86 @@ +# Compiling for Fr OS + +The native backend can emit a static userspace ELF for +[Fr OS](https://github.com/Omena0/fr-os/). + +```sh +python3 src/cli.py compile examples/fr-os/hello.fr -o /tmp/hello.bc +python3 src/cli.py native /tmp/hello.bc -o /tmp/hello.elf --target fr-os +``` + +The result is: + +* `ET_EXEC`, `EM_X86_64`, no `PT_INTERP`; +* linked at the normal userspace base (`0x400000`); +* entered through the global `_start` symbol; +* freestanding (no host libc, no dynamic linker); +* restricted to the Fr OS user ISA profile: SSE2/XMM is allowed, but AVX/AVX2, + FMA and YMM/ZMM are not emitted. Autovectorization is therefore disabled for + this target, and the runtime is compiled with `-mno-avx -mno-avx2 -mno-fma` + plus `-mno-red-zone`. + +## Syscall ABI + +The target uses the numbers in Fr OS's `src/include/uapi/syscall.h`: + +| Operation | Fr OS number | +| --- | --- | +| `read` | 66 | +| `write` | 67 | +| `open` | 64 | +| `close` | 65 | +| `lseek` | 68 | +| `fstat` | 69 | +| `getdents` | 71 | +| `mmap` | 32 | +| `exit` | 2 | +| `fork` | 0 | +| `wait4` | 4 | +| `getpid` | 5 | +| `nanosleep` | 13 | + +The current Fr OS tree only assigns `SYS_socket = 160` and +`SYS_shutdown = 206`; the documented bind/connect/listen/accept/send/recv +numbers are not in the current dispatch table yet. The target uses 161–167 for +those goal-ABI calls, so they return `-ENOSYS` on the current kernel. + +## Known target limitations + +The Fr OS `uapi/syscall.h` currently assigns `fstat = 69` and `getdents = 71`, +while `docs/src/kernel/syscall-abi.md` describes a different numbering +(`stat = 69`, `fstat = 70`, `mkdir = 71`, ...). The new `SYS_shutdown = 206` +also differs from the docs goal's 168. This target follows the uapi +header because that is what the kernel actually dispatches. Path-based +filesystem operations that have no current uapi number (`mkdir`, `rmdir`, +`unlink`, `rename`, `chdir`, `getcwd`) return `-1` in the runtime until the +kernel ABI grows them. `open` is currently a stub in Fr OS and returns +`-ENOENT`, so file tests do not succeed on the current kernel even though the +binary is valid. + +Fr OS userland forbids YMM/ZMM because its context switch uses the legacy +FXSAVE/FXRSTOR image. The compiler's AVX2 vectorizer is deliberately disabled +for this target; use `--target linux` for the AVX2 path. + +## Fr OS updates observed + +The latest Fr OS tree adds termios/TCSETS handling, user signal dispositions and +delivery, `uname`/`sysinfo`, `getrlimit`/`setrlimit`, fd flags with `dup3` and +`fcntl`, keyboard/mouse input state, graphics presentation, and a `shutdown` +syscall at number 206. The target follows the current `uapi/syscall.h` for all +of these (notably `SYS_shutdown = 206`). + +`open` is still a stub returning `-ENOENT`, and the current dispatch table only +assigns `SYS_socket = 160`; the documented bind/connect/listen/accept/send/recv +numbers are future ABI. The compiler keeps those calls behind the goal-ABI +numbers so they will start working when the kernel installs them, but they +return `-ENOSYS` today. + +## Checking an image + +Use Fr OS's own verifier: + +```sh +python3 /path/to/fr-os/tools/verify_isa.py --profile user /tmp/hello.elf +``` + +The generated image should report that it stays inside the 128-bit XMM state. diff --git a/doc/ir_ops.md b/doc/ir_ops.md index e19c752..cca0cee 100644 --- a/doc/ir_ops.md +++ b/doc/ir_ops.md @@ -237,7 +237,10 @@ Bitwise AND, OR, XOR operations. ## Struct Operations -Structs are heap-allocated with 8-byte-aligned fields. The struct pointer model uses a bump allocator in BSS (`struct_data`). +Structs are heap-allocated with 8-byte-aligned fields. The code generator calls +`runtime_struct_new`, which uses the native runtime's segregated free-list +allocator. Non-escaping allocations are replaced by SSA values (SROA) or placed +in the function's stack frame; only escaping structs reach the runtime heap. ### ALLOC_STRUCT @@ -263,6 +266,62 @@ STORE_FIELD %struct_ptr %value Stores a value to a struct field. `*(struct_ptr + field_index * 8) = value`. Void (no result). Side-effecting. + +## Pointer Operations + +Language-level pointers are lowered to these operations. The escape analysis +and register allocator take care of deciding where the target cell lives. + +### ALLOCA + +``` +%r = ALLOCA +``` + +Allocates an 8-byte addressable cell and returns a `*T` pointer. Used for +address-taken locals that cannot stay in SSA form. + +### ADDR_GLOBAL + +``` +%r = ADDR_GLOBAL "" +``` + +Returns the address of a global variable. Uses `imm_str` for the name. + +### PTR_LOAD + +``` +%r = PTR_LOAD %ptr +``` + +Loads the pointee. Reads memory, so it is not CSE'd across stores. + +### PTR_STORE + +``` +PTR_STORE %ptr %value +``` + +Stores through a pointer. Void and side-effecting. + +### FIELD_ADDR + +``` +%r = FIELD_ADDR %struct_ptr +``` + +Returns `struct_ptr + field_index * 8` as a pointer to the field cell. + +### LIST_ELEM_ADDR + +``` +%r = LIST_ELEM_ADDR %list %index +``` + +Returns a pointer to a list element through `runtime_list_elem_addr`. The +pointer is invalidated if the list grows. + ## List Operations Lists are runtime-managed dynamic arrays (`runtime_list_new`, `runtime_list_append_int`, etc.). diff --git a/doc/memory_and_pointers.md b/doc/memory_and_pointers.md new file mode 100644 index 0000000..a032df7 --- /dev/null +++ b/doc/memory_and_pointers.md @@ -0,0 +1,117 @@ +# Memory Model and Pointers + +Fr uses **reference semantics for mutable aggregates** (`struct`, `list`, `set`). +Assigning one to another aliases the same object; the compiler/runtime chooses +where that object lives. + +## Pointer syntax + +Pointers are opt-in. Existing code that does not use `*`/`&` keeps its current +value/reference behaviour. + +```fr +struct Node { + int value + Node* next +} + +void bump(int* p) { + *p = *p + 1 +} + +void main() { + int x = 5 + int* p = &x // address of a local + *p = 9 + println(x) // 9 + + int** pp = &p // pointer to a pointer + **pp = 10 + + int y = 1 + p = &y // re-point at another variable + bump(p) // y == 2 + + list xs = [1, 2, 3] + int* e = &xs[1] // pointer to a list element + *e = 99 +} +``` + +Accepted spellings: + +| Spelling | Meaning | +| --- | --- | +| `int* p` | pointer to `int` | +| `*int p` | pointer to `int` | +| `int** p` / `**int p` | pointer to pointer | +| `Point* p` | pointer to a struct | +| `*Point p` | pointer to a struct | + +`int *p` (with a space before the star) remains the varargs declaration +`int *args`; use `int* p` or `*int p` for pointers. + +Operations: + +* `&lvalue` – address of a variable, struct field, or list element. +* `*p` – load through a pointer. +* `*p = value` – store through a pointer (also `**pp = value`). +* `p.field` automatically dereferences a pointer variable, so `p.x` and + `(*p).x` are equivalent. +* Chained access works: `p.next.value`, `p.next.value = 7`. + +Pointers are raw addresses in the native backend and pointers to `Value` cells +in the C VM. The Python VM uses a `Pointer` object that closes over the target +cell, so `*p = ...` is visible to every alias. + +## Lifetime rules + +* Do not return or store a pointer to a local variable that outlives its frame. + This is the same rule as C. The escape analysis keeps address-taken locals + safe within the function but cannot make a dangling pointer valid. +* Growing a list may reallocate its element buffer, invalidating pointers + previously obtained from `&list[i]`. +* Native heap objects are reference counted with function-scope cleanup. Every + function enters an allocation scope, and every allocation made in that scope + is released when the function returns. Stores into globals, lists and + reference-typed struct fields, as well as returns, retain the value; when the + owning scope exits its reference is dropped and the object is freed if no + other owner remains. Overwriting a field/global releases the old value. +* Cycles between managed objects are not collected, just as with any reference + counting scheme. Explicit pointer fields are borrowed by design; do not build + a cycle through raw pointers unless you manage its lifetime yourself. + +## Automatic memory decisions + +The SSA optimizer runs these memory-related passes: + +1. **SROA (scalar replacement of aggregates)** – non-escaping structs whose + fields are only read/written directly are replaced by SSA values. No memory + allocation remains. +2. **Escape analysis** – classifies each allocation as `NO_ESCAPE`, + `ARG_ESCAPE` (only passed to callees) or `FULL_ESCAPE` (returned, stored in + another object, stored to a global, or address-taken). +3. **Stack allocation** – `ARG_ESCAPE` allocations are placed in fixed slots in + the function frame, so passing a struct to a helper does not allocate. +4. **Runtime allocation and cleanup** – remaining structs are allocated by + `runtime_struct_new`, lists grow with `realloc`, and both the C VM and the + native runtime reference-count structs/lists/sets so aliases share one + object. Native functions push/pop an allocation scope; escaping values are + retained and the rest are freed at scope exit. + +Because structs already have reference semantics, passing a struct to a +function is always a pointer pass; no copy is emitted. Scalar arguments are SSA +values and are not copied either. + +## C VM and Python VM + +* **Python VM** – mutable objects are ordinary Python objects and are managed by + the Python garbage collector. +* **C VM** – `Struct`, `List`, and `Set` carry a reference count. Copies of a + `Value` retain the object; scope/variable cleanup releases it. Cycles are not + collected, matching the usual reference-counting trade-off. +* **Native** – direct pointers, SROA/stack placement where possible, a + segregated free-list heap, and scope-based reference counting for escaping + structs/lists/sets. Values are retained when they escape into a global, list, + reference-typed field, pointer cell, or return value, and released when the + owning function scope ends or an owner is overwritten. diff --git a/doc/optimization.md b/doc/optimization.md new file mode 100644 index 0000000..9630353 --- /dev/null +++ b/doc/optimization.md @@ -0,0 +1,121 @@ +# Compiler Analysis and Optimization + +The native pipeline is: + +``` +source → parse-time evaluation/memoization → bytecode → SSA IR + → interprocedural analysis → specialization/inlining → lookup tables + → register allocation → x86-64 +``` + +## Layers of information + +1. **Parse time** – literal folding, constant-function evaluation and memo tables. + Information that is constant here is already reflected in the bytecode. +2. **SSA local passes** – SCCP, algebraic simplification, branch simplification, + DCE, CSE, LICM, SROA, tail calls, linear block merging. +3. **Whole-program passes** – call graph purity, interprocedural range + propagation, call-site specialization, pure constant-call folding, dead + function removal. +4. **Compile-time simulation** – pure scalar functions are interpreted under a + strict budget and the results become embedded lookup tables. +5. **Code generation** – range facts remove proven-impossible division checks, + and lookup-table fast paths run before the normal function body. Non- + allocating leaf functions omit allocation-scope calls; comparisons fused into + branches avoid setcc/test sequences; `lea` handles integer add/sub copies; + list length and valid list indexing are inlined; live XMM values are + preserved across calls; and a final assembly peephole removes redundant + temporary copies. + +## Purity + +Purity is computed as a greatest fixed point over the call graph. A function is +pure when all of its instructions are pure and every callee is pure. Memory +reads are conservatively treated as impure so reads are never moved across +stores. Pure calls can be removed when unused and are eligible for CSE and +lookup tables. + +## Interprocedural ranges and loop bounds + +`ranges.py` performs a conservative abstract interpretation over integer and +boolean SSA values: + +* arithmetic, comparisons, min/max, abs and boolean operators; +* natural-loop induction-variable bounds (e.g. `for i < 10` gives body range + `0..9`); +* call-site argument ranges propagated into callee parameter ranges; +* widening after a configurable number of visits, with visit/value budgets. + +The loop-body bounds are used to specialize calls inside loops. They are never +used to fold the loop's own exit branch. + +## Call-site specialization + +For every call, the optimizer builds a specification from constant arguments +and inferred ranges. A clone is created only when: + +* at least one argument has a useful fact; +* the callee has multiple blocks (single-block bodies are simply inlined); +* the body is under `max_specialized_body`; +* the specialization count is under `max_specializations`. + +Each clone has its parameters replaced or constrained, then runs SCCP, range +branch folding, unreachable-block removal, linear-block merging and DCE. A +branch containing side effects that is impossible for this call disappears from +the clone. If the clone collapses to one block, the normal inliner merges it +into the caller, so the caller jumps directly past the eliminated checks. + +## Autovectorization + +The native backend recognizes simple list-reduction loops (`sum`, floating sum, +and float sum-of-squares) and emits an AVX2 loop with four lanes and a scalar +remainder. Zero-length lists, non-multiple lengths and negative index edge +cases are handled by the same generated code. The vector loop is emitted only +when the loop has no side effects other than reading the list and updating the +reduction accumulator. + +The generated code preserves all GPR/XMM state, executes `vzeroupper` before +returning to scalar code, and falls back to the normal path for anything that +does not match the reduction pattern. + +## Pure constant-call folding and LICM + +Calls to pure functions whose arguments are all compile-time constants are +evaluated by the budgeted interpreter and replaced with constants. Loop- +invariant arithmetic, loads proven safe from loop-local mutation, and list +length operations are hoisted by LICM. Calls with non-constant arguments still +fall back to the lookup-table fast path. + +## Pure-function lookup tables + +After specialization, live pure scalar functions are analyzed for call-site +constants and ranges. The IR interpreter in `puresim.py` evaluates candidate +inputs (bounded by step, call and wall-clock budgets). Successful results are +emitted as: + +* a contiguous table for dense observed windows, or +* a small compare chain for sparse values. + +The generated function checks the table on entry; misses fall through to the +normal body. The optimization is therefore always semantics-preserving. + +## Tunable budgets + +All speculative work has a budget in `OptimizerConfig` (`src/optimizer/config.py`). +Environment overrides: + +| Variable | Meaning | +| --- | --- | +| `FR_OPT_ITERATIONS` | fixed-point iterations | +| `FR_OPT_INLINE_MAX` | normal inlining size limit | +| `FR_OPT_MAX_SPECIALIZATIONS` | maximum clones per module | +| `FR_OPT_MAX_SPECIALIZED_BODY` | maximum clone source size | +| `FR_OPT_MAX_SPECIALIZED_RANGE` | widest range used for a clone | +| `FR_OPT_RANGE_ITERATIONS` | interprocedural range rounds | +| `FR_OPT_RANGE_VISITS` | abstract interpretation worklist budget | +| `FR_OPT_SIM_STEPS` | compile-time simulation instructions | +| `FR_OPT_SIM_CALLS` | compile-time simulation query budget | +| `FR_OPT_SIM_MS` | per-query simulation wall-clock budget | +| `FR_OPT_TABLE_ENTRIES` | maximum lookup-table entries | +| `FR_OPT_TABLE_DOMAIN` | maximum dense table span | +| `FR_OPT_TABLE_MIN` | minimum entries before emitting a table | diff --git a/doc/vm_instructions.md b/doc/vm_instructions.md index d1476c1..5bed960 100644 --- a/doc/vm_instructions.md +++ b/doc/vm_instructions.md @@ -133,7 +133,7 @@ Can have any num args. In which case it will store all. Load a global variable onto the stack. -**Syntax:** `LOAD_GLOBAL ` +**Syntax:** `LOAD_GLOBAL ` **Stack:** `-> value` @@ -141,10 +141,68 @@ Load a global variable onto the stack. Pop a value from the stack and store it in a global variable. -**Syntax:** `STORE_GLOBAL ` +**Syntax:** `STORE_GLOBAL ` **Stack:** `value ->` + +### ADDR_LOCAL + +Push a pointer to a local variable slot. + +**Syntax:** `ADDR_LOCAL ` + +**Stack:** `-> pointer` + +### ADDR_GLOBAL + +Push a pointer to a global variable cell. + +**Syntax:** `ADDR_GLOBAL ` + +**Stack:** `-> pointer` + +### ADDR_FIELD + +Pop a struct pointer and push a pointer to one of its fields. + +**Syntax:** `ADDR_FIELD ` + +**Stack:** `struct -> pointer` + +### ADDR_ELEM + +Pop a list and index, then push a pointer to the element. + +**Syntax:** `ADDR_ELEM` + +**Stack:** `list index -> pointer` + +### DEREF + +Pop a pointer and push the pointed-to value. + +**Syntax:** `DEREF` + +**Stack:** `pointer -> value` + +### DEREF_SET + +Pop a value then a pointer, and store the value through the pointer. + +**Syntax:** `DEREF_SET` + +**Stack:** `pointer value ->` + +### LIST_NEW_CAP + +Create a fixed-size list whose length is the requested capacity, filled with +type-appropriate zero values. + +**Syntax:** `LIST_NEW_CAP ` + +**Stack:** `-> list` + ### STORE_CONST_I64 Store integer constants directly to variable slots without using the stack. diff --git a/examples/fr-os/README.md b/examples/fr-os/README.md new file mode 100644 index 0000000..81b3bac --- /dev/null +++ b/examples/fr-os/README.md @@ -0,0 +1,13 @@ +# Fr OS example + +Build a static Fr OS userspace ELF: + +```sh +python3 src/cli.py compile examples/fr-os/hello.fr -o /tmp/hello.bc +python3 src/cli.py native /tmp/hello.bc -o /tmp/hello.elf --target fr-os +``` + +Put `/tmp/hello.elf` into a Fr OS initrd with its `tools/initrd.py`, or add it +to the Fr OS Makefile's userland/initrd rules, then boot it under QEMU. + +See `doc/fr-os.md` for the ABI and target limitations. diff --git a/examples/fr-os/hello.fr b/examples/fr-os/hello.fr new file mode 100644 index 0000000..9bca973 --- /dev/null +++ b/examples/fr-os/hello.fr @@ -0,0 +1,4 @@ +// Fr OS userspace hello world. +void main() { + println("hello from Fr OS") +} diff --git a/examples/http/README.md b/examples/http/README.md new file mode 100644 index 0000000..0665f56 --- /dev/null +++ b/examples/http/README.md @@ -0,0 +1,42 @@ +# HTTP library + +`http_lib.fr` is a small HTTP/1.1 server/client library implemented in Fr. It +uses only sockets and string/list operations, so the same source runs on the +Python VM, the C VM, and the native backend. + +## Run + +```sh +# Python VM +python3 src/cli.py examples/http/server_demo.fr -py +python3 src/cli.py examples/http/client_demo.fr -py + +# C VM +python3 src/cli.py compile examples/http/server_demo.fr -o /tmp/server.bc +python3 src/cli.py /tmp/server.bc -c + +# Native +python3 src/cli.py native /tmp/server.bc -o /tmp/server +/tmp/server +``` + +The demo server listens on `127.0.0.1:18081`, handles one request, and exits. +`GET /hello` returns the user agent, `POST /echo` returns the request body, and +everything else returns 404. + +## API + +- `http_parse_request(raw) -> HttpRequest` +- `http_parse_response(raw) -> HttpResponse` +- `http_serialize_response(response) -> str` +- `http_serialize_request(host, method, path, body) -> str` +- `http_read_request(sock) -> HttpRequest` +- `http_read_response(sock) -> HttpResponse` +- `http_send_request(sock, host, method, path, body)` +- `http_send_response(sock, response)` +- `http_get(host, port, path) -> HttpResponse` +- `http_post(host, port, path, body) -> HttpResponse` +- `http_get_header(headers, name) -> str` + +Headers are a list of `"Name: value"` strings so the library works with the +native reference-counted struct fields without a special dict layout. diff --git a/examples/http/client_demo.fr b/examples/http/client_demo.fr new file mode 100644 index 0000000..86cb5e4 --- /dev/null +++ b/examples/http/client_demo.fr @@ -0,0 +1,7 @@ +import http_lib + +void main() { + HttpResponse hello = http_get("127.0.0.1", 18081, "/hello") + println(str(hello.status)) + println(hello.body) +} diff --git a/examples/http/http_lib.fr b/examples/http/http_lib.fr new file mode 100644 index 0000000..a5297c1 --- /dev/null +++ b/examples/http/http_lib.fr @@ -0,0 +1,220 @@ +// A small HTTP/1.1 server/client library. +// +// The library deliberately keeps headers as a list of "Name: value" strings +// rather than a dict inside a struct, so it works identically on the native, +// C and Python runtimes (including native reference-counted cleanup). + +struct HttpRequest { + str method + str path + str version + str body + list headers +} + +struct HttpResponse { + int status + str body + list headers +} + +str http_reason(int status) { + if (status == 200) { return "OK" } + if (status == 201) { return "Created" } + if (status == 204) { return "No Content" } + if (status == 301) { return "Moved Permanently" } + if (status == 302) { return "Found" } + if (status == 304) { return "Not Modified" } + if (status == 400) { return "Bad Request" } + if (status == 401) { return "Unauthorized" } + if (status == 403) { return "Forbidden" } + if (status == 404) { return "Not Found" } + if (status == 405) { return "Method Not Allowed" } + if (status == 500) { return "Internal Server Error" } + return "Unknown" +} + +str http_get_header(list headers, str name) { + str wanted = lower(strip(name)) + int i = 0 + while (i < len(headers)) { + str line = headers[i] + list parts = line.split(":") + if (len(parts) >= 2) { + str key = lower(strip(parts[0])) + if (key == wanted) { + str value = strip(parts[1]) + int j = 2 + while (j < len(parts)) { + value = value + ":" + parts[j] + j = j + 1 + } + return strip(value) + } + } + i = i + 1 + } + return "" +} + +list http_parse_header_lines(list lines) { + list headers = [] + int i = 1 + while (i < len(lines)) { + str line = strip(lines[i]) + if (line == "") { break } + if (contains(line, ":")) { + headers.append(line) + } + i = i + 1 + } + return headers +} + +HttpRequest http_parse_request(str raw) { + list parts = raw.split("\r\n\r\n") + str head = parts[0] + str body = "" + if (len(parts) > 1) { + body = parts[1] + int i = 2 + while (i < len(parts)) { + body = body + "\r\n\r\n" + parts[i] + i = i + 1 + } + } + + list lines = head.split("\r\n") + str method = "GET" + str path = "/" + str version = "HTTP/1.1" + if (len(lines) > 0) { + list start = lines[0].split(" ") + if (len(start) > 0) { method = start[0] } + if (len(start) > 1) { path = start[1] } + if (len(start) > 2) { version = start[2] } + } + + return HttpRequest(method, path, version, body, http_parse_header_lines(lines)) +} + +HttpResponse http_parse_response(str raw) { + list parts = raw.split("\r\n\r\n") + str head = parts[0] + str body = "" + if (len(parts) > 1) { + body = parts[1] + int i = 2 + while (i < len(parts)) { + body = body + "\r\n\r\n" + parts[i] + i = i + 1 + } + } + + list lines = head.split("\r\n") + int status = 0 + if (len(lines) > 0) { + list start = lines[0].split(" ") + if (len(start) > 1) { status = int(start[1]) } + } + + return HttpResponse(status, body, http_parse_header_lines(lines)) +} + +str http_serialize_response(HttpResponse response) { + str out = "HTTP/1.1 " + str(response.status) + " " + http_reason(response.status) + "\r\n" + int i = 0 + bool has_length = false + while (i < len(response.headers)) { + str line = response.headers[i] + list parts = line.split(":") + if (len(parts) > 0) { + if (lower(strip(parts[0])) == "content-length") { has_length = true } + } + out = out + line + "\r\n" + i = i + 1 + } + if (not has_length) { + out = out + "Content-Length: " + str(len(response.body)) + "\r\n" + } + out = out + "Connection: close\r\n\r\n" + response.body + return out +} + +str http_serialize_request(str host, str method, str path, str body) { + str out = method + " " + path + " HTTP/1.1\r\n" + out = out + "Host: " + host + "\r\n" + out = out + "User-Agent: fr-http/1.0\r\n" + out = out + "Content-Length: " + str(len(body)) + "\r\n" + out = out + "Connection: close\r\n\r\n" + body + return out +} + +void http_send_response(int sock, HttpResponse response) { + sendall(sock, http_serialize_response(response).encode("latin-1")) +} + +void http_send_request(int sock, str host, str method, str path, str body) { + sendall(sock, http_serialize_request(host, method, path, body).encode("latin-1")) +} + +str http_read_all(int sock) { + // Accumulate bytes, not decoded chunks: a multi-byte UTF-8 sequence or a + // binary body can be split across recv boundaries. + bytes raw = b"" + while (true) { + bytes chunk = sock.recv(4096) + if (len(chunk) == 0) { break } + raw = raw + chunk + } + return raw.decode("latin-1") +} + +str http_read_headers(int sock) { + bytes raw = b"" + bytes marker = "\r\n\r\n".encode("latin-1") + while (not contains(raw, marker)) { + bytes chunk = sock.recv(4096) + if (len(chunk) == 0) { break } + raw = raw + chunk + } + return raw.decode("latin-1") +} + +HttpRequest http_read_request(int sock) { + str raw = http_read_headers(sock) + HttpRequest request = http_parse_request(raw) + + str content_length = http_get_header(request.headers, "Content-Length") + if (content_length != "") { + int wanted = int(content_length) + while (len(request.body) < wanted) { + bytes chunk = sock.recv(4096) + if (len(chunk) == 0) { break } + request.body = request.body + chunk.decode("latin-1") + } + } + return request +} + +HttpResponse http_read_response(int sock) { + return http_parse_response(http_read_all(sock)) +} + +HttpResponse http_get(str host, int port, str path) { + int sock = socket("inet", "stream") + connect(sock, host, port) + http_send_request(sock, host, "GET", path, "") + HttpResponse response = http_read_response(sock) + sclose(sock) + return response +} + +HttpResponse http_post(str host, int port, str path, str body) { + int sock = socket("inet", "stream") + connect(sock, host, port) + http_send_request(sock, host, "POST", path, body) + HttpResponse response = http_read_response(sock) + sclose(sock) + return response +} diff --git a/examples/http/server_demo.fr b/examples/http/server_demo.fr new file mode 100644 index 0000000..82bad9f --- /dev/null +++ b/examples/http/server_demo.fr @@ -0,0 +1,26 @@ +import http_lib + +void main() { + int server = socket("inet", "stream") + setsockopt(server, "SOL_SOCKET", "SO_REUSEADDR", 1) + bind(server, "127.0.0.1", 18081) + listen(server, 8) + + int client = server.accept() + HttpRequest request = http_read_request(client) + + HttpResponse response = HttpResponse(200, "", []) + response.headers.append("Content-Type: text/plain") + if (request.path == "/hello") { + response.body = "Hello, " + http_get_header(request.headers, "user-agent") + } else if (request.path == "/echo" && request.method == "POST") { + response.body = request.body + } else { + response.status = 404 + response.body = "Not Found" + } + + http_send_response(client, response) + sclose(client) + sclose(server) +} diff --git a/runtime/Makefile b/runtime/Makefile index 97ee2d4..a379f2b 100644 --- a/runtime/Makefile +++ b/runtime/Makefile @@ -2,7 +2,7 @@ CC = gcc PYTHON_CONFIG = python3-config # Use -O3 with -fno-strict-aliasing to prevent issues with type punning -BASE_CFLAGS = -Wall -Wextra -O3 -std=c11 -march=native -mtune=native -fno-strict-aliasing -funroll-loops -finline-functions +BASE_CFLAGS = -Wall -Wextra -O3 -std=c11 -march=native -mtune=native -fno-strict-aliasing -D_GNU_SOURCE -funroll-loops -finline-functions BASE_LDFLAGS = -lm -lgmp ifeq ($(OS),Windows_NT) diff --git a/runtime/fr_rt.c b/runtime/fr_rt.c index be7b72a..f883ddf 100644 --- a/runtime/fr_rt.c +++ b/runtime/fr_rt.c @@ -1,74 +1,189 @@ /* - * Minimal fr runtime - replaces libc with syscall-based implementations. - * Link with: gcc ... runtime/fr_rt.c runtime/syscall.S -nostdlib -static + * Minimal freestanding fr runtime for native binaries. + * + * Linked with: gcc ... runtime/fr_rt.c runtime/syscall.S -nostdlib -static + * + * This file intentionally does not use libc. It provides the small subset of + * the C runtime that the native backend needs: syscalls, a real (free-list + * based) allocator, string/list/set operations, math helpers, file I/O and + * exception unwinding via _setjmp/longjmp. */ #include #include #include -// ============================================================================ -// Syscall wrappers (implemented in assembly) -// ============================================================================ +typedef __builtin_va_list fr_va_list; + +/* Forward declarations for functions used before their definitions. */ +typedef struct { + int64_t* items; + int64_t length; + int64_t capacity; + int elem_type; /* -1 unknown, 0 int, 1 string, 2 float, 3 bool */ +} RuntimeList; + +void* malloc(size_t size); +void free(void* ptr); +void* realloc(void* ptr, size_t size); +void* calloc(size_t count, size_t size); +void runtime_error_at(const char* message, int line); +void runtime_list_append_int(RuntimeList* list, int64_t value); +static void fr_int64_to_buf(int64_t value, char* buffer); +void runtime_retain(void* ptr); +void runtime_release(void* ptr); +void runtime_scope_push(void); +void runtime_scope_pop(void); +void runtime_scope_unwind(int64_t depth); + +/* ------------------------------------------------------------------------- */ +/* Syscalls (implemented in syscall.S) */ +/* ------------------------------------------------------------------------- */ extern int64_t syscall1(int64_t num, int64_t arg1); extern int64_t syscall3(int64_t num, int64_t arg1, int64_t arg2, int64_t arg3); extern int64_t syscall6(int64_t num, int64_t arg1, int64_t arg2, int64_t arg3, int64_t arg4, int64_t arg5, int64_t arg6); -// Syscall numbers -#define SYS_WRITE 1 -#define SYS_EXIT 60 -#define SYS_MMAP 9 -#define SYS_MPROTECT 10 - -// ============================================================================ -// String functions -// ============================================================================ +extern int _setjmp(void* buffer); +extern void longjmp(void* buffer, int value) __attribute__((noreturn)); + +#ifdef FR_TARGET_FR_OS +/* Fr OS uapi/syscall.h (the source of truth in the OS tree). */ +#define SYS_READ 66 +#define SYS_WRITE 67 +#define SYS_CLOSE 65 +#define SYS_LSEEK 68 +#define SYS_MMAP 32 +#define SYS_NANOSLEEP 13 +#define SYS_FORK 0 +#define SYS_EXIT 2 +#define SYS_WAIT4 4 +#define SYS_GETPID 5 +#define SYS_OPEN 64 +#define SYS_FSTAT 69 +#define SYS_GETDENTS 71 +#define SYS_GETCWD (-1) /* not in the current Fr OS uapi */ +#define SYS_CHDIR (-1) +#define SYS_RENAME (-1) +#define SYS_MKDIR (-1) +#define SYS_RMDIR (-1) +#define SYS_UNLINK (-1) +#define SYS_GETDENTS64 SYS_GETDENTS +#define SYS_NEWFSTATAT (-1) +#define SYS_OPENAT SYS_OPEN +#else +#define SYS_READ 0 +#define SYS_WRITE 1 +#define SYS_CLOSE 3 +#define SYS_LSEEK 8 +#define SYS_MMAP 9 +#define SYS_NANOSLEEP 35 +#define SYS_FORK 57 +#define SYS_EXIT 60 +#define SYS_WAIT4 61 +#define SYS_GETCWD 79 +#define SYS_CHDIR 80 +#define SYS_RENAME 82 +#define SYS_MKDIR 83 +#define SYS_RMDIR 84 +#define SYS_UNLINK 87 +#define SYS_GETPID 39 +#define SYS_GETDENTS64 217 +#define SYS_NEWFSTATAT 262 +#define SYS_OPENAT 257 +#endif + +#define AT_FDCWD (-100) +#define O_RDONLY 0 +#define O_WRONLY 1 +#define O_RDWR 2 +#define O_CREAT 64 +#define O_TRUNC 512 +#define O_APPEND 1024 + +#define SEEK_SET 0 +#define SEEK_CUR 1 +#define SEEK_END 2 + +/* ------------------------------------------------------------------------- */ +/* String / memory primitives */ +/* ------------------------------------------------------------------------- */ int64_t strlen(const char* s) { const char* p = s; + if (!p) return 0; while (*p) p++; return (int64_t)(p - s); } -void* memcpy(void* dest, const void* src, int64_t n) { - char* d = (char*)dest; - const char* s = (const char*)src; - while (n--) *d++ = *s++; +void* memcpy(void* dest, const void* src, size_t n) { + unsigned char* d = (unsigned char*)dest; + const unsigned char* s = (const unsigned char*)src; + for (size_t i = 0; i < n; i++) d[i] = s[i]; return dest; } -void* memset(void* dest, int c, int64_t n) { - char* d = (char*)dest; - while (n--) *d++ = (char)c; +void* memmove(void* dest, const void* src, size_t n) { + unsigned char* d = (unsigned char*)dest; + const unsigned char* s = (const unsigned char*)src; + if (d < s) { + for (size_t i = 0; i < n; i++) d[i] = s[i]; + } else if (d > s) { + for (size_t i = n; i > 0; i--) d[i - 1] = s[i - 1]; + } + return dest; +} + +void* memset(void* dest, int c, size_t n) { + unsigned char* d = (unsigned char*)dest; + for (size_t i = 0; i < n; i++) d[i] = (unsigned char)c; return dest; } -int memcmp(const void* a, const void* b, int64_t n) { +int memcmp(const void* a, const void* b, size_t n) { const unsigned char* pa = (const unsigned char*)a; const unsigned char* pb = (const unsigned char*)b; - while (n--) { - if (*pa != *pb) return *pa - *pb; - pa++; pb++; + for (size_t i = 0; i < n; i++) { + if (pa[i] != pb[i]) return (int)pa[i] - (int)pb[i]; + } + return 0; +} + +int strcmp(const char* a, const char* b) { + while (*a && *a == *b) { a++; b++; } + return (unsigned char)*a - (unsigned char)*b; +} + +int strncmp(const char* a, const char* b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (a[i] != b[i]) return (unsigned char)a[i] - (unsigned char)b[i]; + if (!a[i]) return 0; } return 0; } char* strcpy(char* dest, const char* src) { char* d = dest; - while ((*d++ = *src++)); + while ((*d++ = *src++)) {} + return dest; +} + +char* strncpy(char* dest, const char* src, size_t n) { + size_t i = 0; + for (; i < n && src[i]; i++) dest[i] = src[i]; + for (; i < n; i++) dest[i] = '\0'; return dest; } char* strcat(char* dest, const char* src) { char* d = dest + strlen(dest); - while ((*d++ = *src++)); + while ((*d++ = *src++)) {} return dest; } const char* strchr(const char* s, int c) { - while (*s) { + while (s && *s) { if ((unsigned char)*s == (unsigned char)c) return s; s++; } @@ -76,6 +191,7 @@ const char* strchr(const char* s, int c) { } const char* strstr(const char* haystack, const char* needle) { + if (!haystack || !needle) return NULL; if (!*needle) return haystack; for (; *haystack; haystack++) { const char* h = haystack; @@ -86,476 +202,2153 @@ const char* strstr(const char* haystack, const char* needle) { return NULL; } -// ============================================================================ -// Stdio replacements -// ============================================================================ +/* ------------------------------------------------------------------------- */ +/* Output */ +/* ------------------------------------------------------------------------- */ + +static void fr_write(int fd, const char* data, size_t len) { + size_t off = 0; + while (off < len) { + int64_t n = syscall3(SYS_WRITE, fd, (int64_t)(data + off), + (int64_t)(len - off)); + if (n <= 0) break; + off += (size_t)n; + } +} void fr_putchar(char c) { - char buf[1]; - buf[0] = c; - syscall3(SYS_WRITE, 1, (int64_t)buf, 1); + fr_write(1, &c, 1); } void fr_puts(const char* s) { - int64_t len = strlen(s); - syscall3(SYS_WRITE, 1, (int64_t)s, len); - syscall3(SYS_WRITE, 1, (int64_t)"\n", 1); + if (!s) return; + fr_write(1, s, (size_t)strlen(s)); + fr_putchar('\n'); } void fr_fputs(const char* s, int fd) { - int64_t len = strlen(s); - syscall3(SYS_WRITE, fd, (int64_t)s, len); + if (!s) return; + fr_write(fd, s, (size_t)strlen(s)); } -void fr_fflush(int fd) { - (void)fd; -} +void fr_fflush(int fd) { (void)fd; } void fr_exit(int status) { syscall1(SYS_EXIT, status); - while (1); + while (1) {} } -// Minimal printf - handles %s, %ld, %g -void fr_printf(const char* fmt, ...) { - // Simple implementation - just walk the format string - // For now, this is a stub that works for our basic cases -} - -// Provide libc-compatible names for assembly +/* libc-compatible aliases used by emitted assembly */ void puts(const char* s) { fr_puts(s); } void fputs(const char* s, int fd) { fr_fputs(s, fd); } void fflush(int fd) { fr_fflush(fd); } void exit(int status) { fr_exit(status); } -void printf(const char* fmt, ...) { fr_printf(fmt); } - -// ============================================================================ -// Memory allocation -// ============================================================================ - -static uint8_t* heap_base; -static uint8_t* heap_ptr; -static uint64_t heap_size; - -void init_heap(void) { - heap_base = (uint8_t*)syscall6(SYS_MMAP, 0, 8 * 1024 * 1024, - 0x3 /* PROT_READ|PROT_WRITE */, - 0x22 /* MAP_PRIVATE|MAP_ANONYMOUS */, - -1, 0); - heap_ptr = heap_base; - heap_size = 8 * 1024 * 1024; -} - -void* malloc(int64_t size) { - size = (size + 7) & ~7; - if (heap_ptr + size > heap_base + heap_size) { - return NULL; - } - void* ptr = heap_ptr; - heap_ptr += size; - return ptr; -} - -void free(void* ptr) { - (void)ptr; -} - -// ============================================================================ -// Math functions -// ============================================================================ - -double fabs(double x) { - if (x < 0) return -x; - return x; -} - -double sqrt(double x) { - if (x <= 0) return 0; - double guess = x / 2; - for (int i = 0; i < 20; i++) { - guess = (guess + x / guess) / 2; - } - return guess; -} - -double floor(double x) { - int64_t i = (int64_t)x; - return (double)i; -} - -double ceil(double x) { - int64_t i = (int64_t)x; - if (x == (double)i) return (double)i; - return (double)(i + 1); -} - -double round(double x) { - if (x >= 0) return floor(x + 0.5); - return ceil(x - 0.5); -} -double pow(double base, double exp) { - if (exp == 0) return 1; - if (exp == 1) return base; - double result = 1; - int64_t e = (int64_t)exp; - double b = base; - while (e > 0) { - if (e & 1) result *= b; - b *= b; - e >>= 1; +static void fr_write_int(int64_t value, bool newline) { + char buf[32]; + char* p = buf + sizeof(buf); + *--p = '\0'; + uint64_t n = value < 0 ? (uint64_t)(-(value + 1)) + 1 : (uint64_t)value; + if (n == 0) { + *--p = '0'; + } else { + while (n) { + *--p = (char)('0' + (n % 10)); + n /= 10; + } + if (value < 0) *--p = '-'; } - return result; + fr_write(1, p, (size_t)(buf + sizeof(buf) - 1 - p)); + if (newline) fr_putchar('\n'); } -double sin(double x) { return x; } -double cos(double x) { return 1; } -double tan(double x) { return x; } - -// ============================================================================ -// Exception handling stubs -// ============================================================================ - -#define MAX_EXCEPTION_HANDLERS 256 -static void* exception_handlers[MAX_EXCEPTION_HANDLERS]; -static int exception_handler_count = 0; - -// ============================================================================ -// Type conversion stubs -// ============================================================================ - char* runtime_int_to_str(int64_t value) { - char* buf = malloc(32); - if (!buf) return NULL; - char* p = buf + 31; + char* result = (char*)malloc(32); + if (!result) return NULL; + char* p = result + 31; *p = '\0'; - if (value == 0) { + uint64_t n = value < 0 ? (uint64_t)(-(value + 1)) + 1 : (uint64_t)value; + if (n == 0) { *--p = '0'; } else { - int64_t n = value; - if (n < 0) n = -n; - while (n > 0) { - *--p = '0' + (n % 10); + while (n) { + *--p = (char)('0' + (n % 10)); n /= 10; } if (value < 0) *--p = '-'; } - return p; + size_t len = (size_t)(result + 31 - p); + memmove(result, p, len + 1); + return result; } char* runtime_float_to_str(double value) { - char* buf = malloc(64); - if (!buf) return NULL; - int64_t ipart = (int64_t)value; - double fracpart = value - (double)ipart; - char* p = buf; + char* result = (char*)malloc(64); + if (!result) return NULL; + + if (!(value == value)) { /* NaN */ + strcpy(result, "nan"); + return result; + } + if (value > 1.7976931348623157e308) { + strcpy(result, "inf"); + return result; + } + if (value < -1.7976931348623157e308) { + strcpy(result, "-inf"); + return result; + } + + char* out = result; if (value < 0) { - *p++ = '-'; - ipart = -ipart; - fracpart = -fracpart; + *out++ = '-'; + value = -value; } + + /* Split into integer and fractional parts. */ + uint64_t ipart = (uint64_t)value; + double frac = value - (double)ipart; + char tmp[32]; - char* t = tmp + 31; - *t = '\0'; + char* t = tmp + sizeof(tmp); + *--t = '\0'; if (ipart == 0) { *--t = '0'; } else { - int64_t n = ipart; - while (n > 0) { - *--t = '0' + (n % 10); - n /= 10; + while (ipart) { + *--t = (char)('0' + (ipart % 10)); + ipart /= 10; } } - while (*t) *p++ = *t++; - *p++ = '.'; - for (int i = 0; i < 6; i++) { - fracpart *= 10; - int digit = (int)fracpart; - *p++ = '0' + digit; - fracpart -= digit; - if (fracpart < 0.000001) break; + while (*t) *out++ = *t++; + + *out++ = '.'; + if (frac < 1e-12) { + *out++ = '0'; + } else { + /* Produce six fractional digits, then round the final digit. */ + char digits[6]; + for (int i = 0; i < 6; i++) { + frac *= 10.0; + int digit = (int)frac; + digits[i] = (char)('0' + digit); + frac -= (double)digit; + } + if (frac >= 0.5) { + int i = 5; + while (i >= 0 && digits[i] == '9') { + digits[i] = '0'; + i--; + } + if (i >= 0) { + digits[i]++; + } else { + /* Carry into the integer part: re-render it. */ + char* ip = out; + while (ip > result && ip[-1] != '.' && ip[-1] != '-') ip--; + /* Find the start of the integer text. */ + char* int_start = result; + if (*int_start == '-') int_start++; + char* p2 = out - 1; /* points at '.' */ + while (p2 > int_start) { + p2--; + if (*p2 == '9') { + *p2 = '0'; + } else { + (*p2)++; + break; + } + } + if (p2 == int_start) { + /* All nines: prepend a 1. */ + memmove(int_start + 1, int_start, (size_t)(out - int_start)); + *int_start = '1'; + out++; + } + } + } + int last = 5; + while (last >= 0 && digits[last] == '0') last--; + for (int i = 0; i <= last; i++) *out++ = digits[i]; + if (last < 0) *out++ = '0'; } - while (*(p-1) == '0') p--; - if (*(p-1) == '.') *p++ = '0'; - *p = '\0'; - return buf; + *out = '\0'; + return result; } char* runtime_bool_to_str(int64_t value) { - if (value) return (char*)"true"; - return (char*)"false"; + return (char*)(value ? "true" : "false"); } -char* runtime_str_concat(const char* a, const char* b) { - int64_t len_a = strlen(a); - int64_t len_b = strlen(b); - char* result = malloc(len_a + len_b + 1); - if (!result) return NULL; - memcpy(result, a, len_a); - memcpy(result + len_a, b, len_b); - result[len_a + len_b] = '\0'; - return result; +/* Minimal printf: %s %c %d %i %u %ld %lu %f %g %x %% */ +int printf(const char* fmt, ...) { + fr_va_list ap; + __builtin_va_start(ap, fmt); + + for (const char* p = fmt; *p; p++) { + if (*p != '%') { + fr_putchar(*p); + continue; + } + p++; + if (*p == '%') { + fr_putchar('%'); + continue; + } + bool is_long = false; + while (*p == 'l' || *p == 'z') { + is_long = true; + p++; + } + switch (*p) { + case 'd': + case 'i': { + int64_t v = is_long ? __builtin_va_arg(ap, int64_t) + : (int64_t)__builtin_va_arg(ap, int); + fr_write_int(v, false); + break; + } + case 'u': { + uint64_t v = is_long ? __builtin_va_arg(ap, uint64_t) + : (uint64_t)__builtin_va_arg(ap, unsigned int); + fr_write_int((int64_t)v, false); + break; + } + case 'x': { + uint64_t v = is_long ? __builtin_va_arg(ap, uint64_t) + : (uint64_t)__builtin_va_arg(ap, unsigned int); + char buf[24]; + char* q = buf + sizeof(buf); + *--q = '\0'; + if (!v) *--q = '0'; + while (v) { + unsigned d = (unsigned)(v & 15u); + *--q = (char)(d < 10 ? '0' + d : 'a' + d - 10); + v >>= 4; + } + fr_write(1, q, (size_t)(buf + sizeof(buf) - 1 - q)); + break; + } + case 's': { + const char* s = __builtin_va_arg(ap, const char*); + if (!s) s = "(null)"; + fr_write(1, s, (size_t)strlen(s)); + break; + } + case 'c': + fr_putchar((char)__builtin_va_arg(ap, int)); + break; + case 'f': + case 'g': { + double d = __builtin_va_arg(ap, double); + char* s = runtime_float_to_str(d); + if (s) { + fr_write(1, s, (size_t)strlen(s)); + free(s); + } + break; + } + default: + fr_putchar('%'); + if (*p) fr_putchar(*p); + break; + } + } + __builtin_va_end(ap); + return 0; } -int64_t runtime_str_len(const char* str) { - return strlen(str); +void fr_printf(const char* fmt, ...) { (void)fmt; } + +/* ------------------------------------------------------------------------- */ +/* Allocator: mmap arena + segregated free lists. */ +/* ------------------------------------------------------------------------- */ + +#define HEAP_SIZE ((size_t)64 * 1024 * 1024) +#define HEAP_MAGIC 0x4652484541503031ULL /* "FRHEAP01" */ +#define MIN_BLOCK 16 +#define NUM_BINS 32 + +typedef struct BlockHeader { + uint64_t magic; + size_t size; /* payload size */ + size_t bin; +} BlockHeader; + +typedef struct FreeBlock { + struct FreeBlock* next; +} FreeBlock; + +static unsigned char* heap_base; +static unsigned char* heap_cur; +static unsigned char* heap_end; +static FreeBlock* free_lists[NUM_BINS]; + +static int bin_index(size_t size) { + size_t block = size + sizeof(BlockHeader); + int bin = 4; /* 16-byte minimum */ + size_t cap = (size_t)1 << bin; + while (cap < block && bin < NUM_BINS - 1) { + bin++; + cap <<= 1; + } + return bin; } -int64_t runtime_str_contains(const char* haystack, const char* needle) { - return strstr(haystack, needle) != NULL ? 1 : 0; +static size_t bin_size(int bin) { + return (size_t)1 << bin; } -// ============================================================================ -// List operations -// ============================================================================ +void init_heap(void) { + if (heap_base) return; + int64_t result = syscall6(SYS_MMAP, 0, (int64_t)HEAP_SIZE, + 3 /* PROT_READ|PROT_WRITE */, + 0x22 /* MAP_PRIVATE|MAP_ANONYMOUS */, -1, 0); + if (result < 0) { + fr_fputs("Runtime error: out of memory\n", 2); + fr_exit(1); + } + heap_base = (unsigned char*)result; + heap_cur = heap_base; + heap_end = heap_base + HEAP_SIZE; + for (int i = 0; i < NUM_BINS; i++) free_lists[i] = NULL; +} -typedef struct { - int64_t* items; - int64_t length; - int64_t capacity; - int elem_type; -} RuntimeList; +void* malloc(size_t size) { + if (!heap_base) init_heap(); + if (size == 0) size = 8; + int bin = bin_index(size); + if (bin >= NUM_BINS) { + fr_fputs("Runtime error: allocation too large\n", 2); + fr_exit(1); + } -RuntimeList* runtime_list_new(void) { - RuntimeList* list = (RuntimeList*)malloc(sizeof(RuntimeList)); - if (!list) return NULL; - list->capacity = 8; - list->length = 0; - list->elem_type = -1; - list->items = (int64_t*)malloc(list->capacity * sizeof(int64_t)); - if (!list->items) { free(list); return NULL; } - return list; + BlockHeader* header; + if (free_lists[bin]) { + FreeBlock* block = free_lists[bin]; + free_lists[bin] = block->next; + header = (BlockHeader*)block; + } else { + size_t total = bin_size(bin); + if (heap_cur + total > heap_end) { + /* Try a larger bin before giving up. */ + int alt = bin + 1; + while (alt < NUM_BINS && !free_lists[alt]) alt++; + if (alt < NUM_BINS) { + FreeBlock* block = free_lists[alt]; + free_lists[alt] = block->next; + header = (BlockHeader*)block; + /* A larger block is fine; free() records its actual bin. */ + header->magic = HEAP_MAGIC; + header->size = bin_size(alt) - sizeof(BlockHeader); + header->bin = (size_t)alt; + return (void*)(header + 1); + } + fr_fputs("Runtime error: out of memory\n", 2); + fr_exit(1); + } + header = (BlockHeader*)heap_cur; + heap_cur += total; + } + + header->magic = HEAP_MAGIC; + header->size = bin_size(bin) - sizeof(BlockHeader); + header->bin = (size_t)bin; + return (void*)(header + 1); } -void runtime_list_append_int(RuntimeList* list, int64_t value) { - if (list->length >= list->capacity) { - list->capacity *= 2; - list->items = (int64_t*)malloc(list->capacity * sizeof(int64_t)); - if (!list->items) return; +void free(void* ptr) { + if (!ptr) return; + BlockHeader* header = ((BlockHeader*)ptr) - 1; + if (header->magic != HEAP_MAGIC) { + fr_fputs("Runtime error: invalid free\n", 2); + fr_exit(1); } - list->items[list->length++] = value; + size_t bin = header->bin; + if (bin >= NUM_BINS) return; + header->magic = 0; + FreeBlock* block = (FreeBlock*)header; + block->next = free_lists[bin]; + free_lists[bin] = block; } -int64_t runtime_list_get_int(RuntimeList* list, int64_t index) { - if (index < 0) index = list->length + index; - if (index < 0 || index >= list->length) return 0; - return list->items[index]; +void* realloc(void* ptr, size_t size) { + if (!ptr) return malloc(size); + if (size == 0) { + free(ptr); + return NULL; + } + BlockHeader* header = ((BlockHeader*)ptr) - 1; + if (header->magic != HEAP_MAGIC) return malloc(size); + if (header->size >= size) return ptr; + + void* next = malloc(size); + if (!next) return NULL; + memcpy(next, ptr, header->size); + free(ptr); + return next; } -void runtime_list_set_int(RuntimeList* list, int64_t index, int64_t value) { - if (index < 0) index = list->length + index; - if (index < 0 || index >= list->length) return; - list->items[index] = value; +void* calloc(size_t count, size_t size) { + size_t total = count * size; + void* ptr = malloc(total); + if (ptr) memset(ptr, 0, total); + return ptr; } -int64_t runtime_list_len(RuntimeList* list) { - return list->length; +/* + * Managed objects + * --------------- + * Structs and lists carry a header so the runtime can use reference counting + * with deterministic, scope-based cleanup. Every allocation is registered in + * the current function's allocation scope. Escaping values are retained by the + * operation that makes them escape (field/global/list store or return); when + * the owning function exits its scope drops one reference from every object it + * created. Objects still referenced elsewhere survive, unreferenced ones are + * freed immediately. + */ +#define FR_OBJ_MAGIC 0x46524F424A303031ULL /* "FROBJ001" */ +#define FR_OBJ_STRUCT 1 +#define FR_OBJ_LIST 2 +#define FR_OBJ_SET 3 +#define FR_OBJ_RAW 4 +#define FR_OBJ_DICT 5 + +typedef struct FrObjHeader { + uint64_t magic; + int64_t refcount; + int64_t kind; + int64_t aux; /* struct: managed-field mask */ + struct FrObjHeader* next_scope; + struct FrObjHeader* prev_scope; + int64_t scope_id; + uint64_t reserved; +} FrObjHeader; + +#define FR_MAX_SCOPES 1024 +FrObjHeader* fr_scope_heads[FR_MAX_SCOPES]; +int64_t fr_scope_depth = 0; +int64_t fr_scope_overflow = 0; + +static bool fr_obj_valid(void* ptr) { + if (!ptr || !heap_base) return false; + unsigned char* p = (unsigned char*)ptr; + if (p < heap_base + sizeof(FrObjHeader) || p >= heap_cur) return false; + FrObjHeader* header = ((FrObjHeader*)p) - 1; + return header->magic == FR_OBJ_MAGIC; } -char* runtime_list_to_str(RuntimeList* list) { - (void)list; - return (char*)"[list]"; +static void* fr_obj_alloc(size_t size, int kind, int64_t aux) { + FrObjHeader* header = (FrObjHeader*)malloc(sizeof(FrObjHeader) + size); + if (!header) return NULL; + memset(header, 0, sizeof(FrObjHeader)); + header->magic = FR_OBJ_MAGIC; + header->refcount = 1; + header->kind = kind; + header->aux = aux; + header->next_scope = NULL; + header->prev_scope = NULL; + header->scope_id = -1; + if (fr_scope_depth > 0) { + int depth = fr_scope_depth - 1; + header->scope_id = depth; + header->next_scope = fr_scope_heads[depth]; + if (fr_scope_heads[depth]) fr_scope_heads[depth]->prev_scope = header; + fr_scope_heads[depth] = header; + } + return (void*)(header + 1); } -// ============================================================================ -// Set operations -// ============================================================================ +void runtime_retain(void* ptr) { + if (!fr_obj_valid(ptr)) return; + FrObjHeader* header = ((FrObjHeader*)ptr) - 1; + header->refcount++; +} -typedef struct { - int64_t* items; - int64_t length; - int64_t capacity; - int elem_type; -} RuntimeSet; +/* Remove an object from the current scope without freeing it. Used when a + * store/return transfers the current scope's reference to a new owner. */ +static void runtime_scope_forget(void* ptr) { + if (!fr_obj_valid(ptr) || fr_scope_depth <= 0) return; + FrObjHeader* header = ((FrObjHeader*)ptr) - 1; + int depth = fr_scope_depth - 1; + if (header->scope_id != depth) return; + + if (header->prev_scope) { + header->prev_scope->next_scope = header->next_scope; + } else if (fr_scope_heads[depth] == header) { + fr_scope_heads[depth] = header->next_scope; + } + if (header->next_scope) { + header->next_scope->prev_scope = header->prev_scope; + } + header->next_scope = NULL; + header->prev_scope = NULL; + header->scope_id = -1; + runtime_release(ptr); +} -RuntimeSet* runtime_set_new(void) { - RuntimeSet* set = (RuntimeSet*)malloc(sizeof(RuntimeSet)); - if (!set) return NULL; - set->capacity = 16; - set->length = 0; - set->elem_type = -1; - set->items = (int64_t*)malloc(sizeof(int64_t) * set->capacity); - if (!set->items) { free(set); return NULL; } - return set; +void runtime_retain_transfer(void* ptr) { + runtime_retain(ptr); + runtime_scope_forget(ptr); } -void runtime_set_add(RuntimeSet* set, int64_t value) { - if (set->length >= set->capacity) { - set->capacity *= 2; - set->items = (int64_t*)malloc(sizeof(int64_t) * set->capacity); - if (!set->items) return; +void runtime_scope_pop(void) { + if (fr_scope_overflow > 0) { + fr_scope_overflow--; + return; + } + if (fr_scope_depth <= 0) return; + int depth = --fr_scope_depth; + FrObjHeader* header = fr_scope_heads[depth]; + fr_scope_heads[depth] = NULL; + while (header) { + FrObjHeader* next = header->next_scope; + header->next_scope = NULL; + runtime_release((void*)(header + 1)); + header = next; } - set->items[set->length++] = value; } -int64_t runtime_set_len(RuntimeSet* set) { - return set->length; +void runtime_scope_unwind(int64_t depth) { + if (depth < 0) depth = 0; + while (fr_scope_overflow > 0) runtime_scope_pop(); + while (fr_scope_depth > depth) runtime_scope_pop(); } -char* runtime_set_to_str(RuntimeSet* set) { - (void)set; - return (char*)"{set}"; +void* runtime_alloc(int64_t size) { + if (size <= 0) size = 8; + return fr_obj_alloc((size_t)size, FR_OBJ_RAW, 0); } -// ============================================================================ -// Error handling and exceptions -// ============================================================================ +void runtime_free(void* ptr) { runtime_release(ptr); } +void* runtime_realloc(void* ptr, int64_t size) { return realloc(ptr, (size_t)size); } -void int64_to_str(int64_t value, char* buffer) { - char* p = buffer + 31; - *p = '\0'; - if (value == 0) { - *--p = '0'; - } else { - int64_t n = value; - if (n < 0) n = -n; - while (n > 0) { - *--p = '0' + (n % 10); - n /= 10; +void* runtime_struct_new(int64_t size, int64_t count, int64_t* fields, + int64_t managed_mask) { + char* memory = (char*)fr_obj_alloc((size_t)size, FR_OBJ_STRUCT, managed_mask); + if (!memory) return NULL; + if (fields && count > 0) { + memcpy(memory, fields, (size_t)count * sizeof(int64_t)); + } + for (int i = 0; i < 64; i++) { + if (managed_mask & ((int64_t)1 << i)) { + runtime_retain(((void**)memory)[i]); + runtime_scope_forget(((void**)memory)[i]); } - if (value < 0) *--p = '-'; } - while (*p) *buffer++ = *p++; - *buffer = '\0'; + return memory; } -void runtime_error_at(const char* message, int line) { - char full_msg[1024]; - char line_str[32]; - int64_to_str(line, line_str); - strcpy(full_msg, "?"); - strcat(full_msg, line_str); - strcat(full_msg, ",0:"); - strcat(full_msg, message); - strcat(full_msg, "\n"); - fr_fputs(full_msg, 2); - fr_exit(1); +void runtime_store_field(void* struct_ptr, int64_t value, int64_t field_index, + int64_t managed) { + if (!struct_ptr) return; + int64_t* fields = (int64_t*)struct_ptr; + if (managed) { + if (fields[field_index] == value) return; + runtime_retain((void*)value); + if (fr_obj_valid(struct_ptr)) { + FrObjHeader* header = ((FrObjHeader*)struct_ptr) - 1; + if (header->kind == FR_OBJ_STRUCT) runtime_scope_forget((void*)value); + } + runtime_release((void*)fields[field_index]); + } + fields[field_index] = value; } -void runtime_exception_init(void) { +void runtime_store_global(int64_t* slot, int64_t value, int64_t managed) { + if (!slot) return; + if (managed) { + if (*slot == value) return; + runtime_retain((void*)value); + runtime_scope_forget((void*)value); + runtime_release((void*)*slot); + } + *slot = value; } -void* runtime_exception_push(const char* exc_type) { - (void)exc_type; - return NULL; +void runtime_ptr_store(int64_t* slot, int64_t value, int64_t managed) { + runtime_store_global(slot, value, managed); } -void runtime_exception_pop(void) { +/* ------------------------------------------------------------------------- */ +/* Math */ +/* ------------------------------------------------------------------------- */ + +double fabs(double x) { return x < 0 ? -x : x; } + +double sqrt(double x) { + if (x <= 0.0) return 0.0; + double result; + __asm__ __volatile__("sqrtsd %1, %0" : "=x"(result) : "x"(x)); + return result; } -void runtime_exception_raise(const char* exc_type, const char* message) { - (void)exc_type; - fr_printf("Uncaught exception: [%s] %s\n", exc_type, message); - fr_exit(1); +double floor(double x) { + int64_t i = (int64_t)x; + if ((double)i > x) i--; + return (double)i; } -void runtime_exception_raise_at(const char* exc_type, const char* message, int line) { - char full_msg[1024]; - char line_str[32]; - int64_to_str(line, line_str); - strcpy(full_msg, "?"); - strcat(full_msg, line_str); - strcat(full_msg, ",0:["); - strcat(full_msg, exc_type); - strcat(full_msg, "] "); - strcat(full_msg, message); - strcat(full_msg, "\n"); - fr_fputs(full_msg, 2); - fr_exit(1); +double ceil(double x) { + int64_t i = (int64_t)x; + if ((double)i < x) i++; + return (double)i; } -void runtime_check_div_zero_i64_at(int64_t divisor, int line) { - if (divisor == 0) { - runtime_error_at("integer division by zero", line); - } +double round(double x) { + return x >= 0.0 ? floor(x + 0.5) : ceil(x - 0.5); } -void runtime_check_div_zero_f64_at(double divisor, int line) { - if (divisor == 0.0) { - runtime_error_at("float division by zero", line); - } +double runtime_floor(double x) { return floor(x); } +double runtime_ceil(double x) { return ceil(x); } +double runtime_round(double x) { return round(x); } +int64_t runtime_round_int(double x) { return (int64_t)round(x); } + +double runtime_sin(double x) { + double r; + __asm__ __volatile__("fsin" : "=t"(r) : "0"(x)); + return r; } -// ============================================================================ -// List operations with bounds checking -// ============================================================================ +double runtime_cos(double x) { + double r; + __asm__ __volatile__("fcos" : "=t"(r) : "0"(x)); + return r; +} -int64_t runtime_list_get_int_at(RuntimeList* list, int64_t index, int line) { - if (!list) { - runtime_error_at("Index error: null list pointer", line); - } - int64_t original_index = index; - if (index < 0) { - index = list->length + index; - } - if (index < 0 || index >= list->length) { - char msg[512]; - char index_str[32]; - char length_str[32]; - int64_to_str(original_index, index_str); - int64_to_str(list->length, length_str); - strcpy(msg, "Index error: list index out of range: "); - strcat(msg, index_str); - strcat(msg, " (length: "); - strcat(msg, length_str); - strcat(msg, ")"); - runtime_error_at(msg, line); - } - return list->items[index]; +double runtime_tan(double x) { + if (x == 0.0) return 0.0; + double r; + __asm__ __volatile__("fptan\n\tfstp %%st(0)" : "=t"(r) : "0"(x) : "st(1)"); + return r; } -void runtime_list_set_int_at(RuntimeList* list, int64_t index, int64_t value, int line) { - int64_t original_index = index; - if (index < 0) { - index = list->length + index; +double sin(double x) { return runtime_sin(x); } +double cos(double x) { return runtime_cos(x); } +double tan(double x) { return runtime_tan(x); } + +double pow(double base, double exp) { + if (exp == 0.0) return 1.0; + if (exp == 1.0) return base; + if (base == 0.0) return 0.0; + + bool neg = exp < 0.0; + double e = neg ? -exp : exp; + int64_t whole = (int64_t)e; + double fraction = e - (double)whole; + + double result = 1.0; + double b = base; + int64_t n = whole; + while (n > 0) { + if (n & 1) result *= b; + b *= b; + n >>= 1; } - if (index < 0 || index >= list->length) { - char msg[512]; - char index_str[32]; - char length_str[32]; - int64_to_str(original_index, index_str); - int64_to_str(list->length, length_str); - strcpy(msg, "Index error: list index out of range: "); - strcat(msg, index_str); - strcat(msg, " (length: "); - strcat(msg, length_str); - strcat(msg, ")"); - runtime_error_at(msg, line); + /* Fractional exponent: use exp(log(x)) via x87. */ + if (fraction > 0.0) { + double lx; + __asm__ __volatile__("fyl2x" + : "=t"(lx) + : "0"(base), "u"(fraction) + : "st(1)"); + double ex; + __asm__ __volatile__("fldl2e; fmulp; fld %%st(0); frndint; " + "fxch %%st(1); fsub %%st(1), %%st; f2xm1; " + "fld1; faddp; fscale; fstp %%st(1)" + : "=t"(ex) + : "0"(lx) + : "st(1)"); + result *= ex; } - list->items[index] = value; + return neg ? 1.0 / result : result; } -RuntimeList* runtime_list_from_array(int64_t* values, int64_t count) { - RuntimeList* list = runtime_list_new(); - if (!list) return NULL; - list->capacity = count; - list->length = count; - list->items = (int64_t*)malloc(count * sizeof(int64_t)); - if (!list->items) { free(list); return NULL; } - memcpy(list->items, values, count * sizeof(int64_t)); - return list; -} +double runtime_pow(double base, double exp) { return pow(base, exp); } -// Runtime init - must be non-static for assembly call -void runtime_init(void) { - init_heap(); - runtime_exception_init(); -} +int64_t runtime_abs_int(int64_t value) { return value < 0 ? -value : value; } +int64_t runtime_min_int(int64_t a, int64_t b) { return a < b ? a : b; } +int64_t runtime_max_int(int64_t a, int64_t b) { return a > b ? a : b; } -void runtime_assert(bool condition, const char* message) { - if (!condition) { - if (message && message[0] != '\0') { - fr_fputs(message, 2); - fr_putchar('\n'); - } else { - fr_fputs("Assertion failed\n", 2); - } - fr_exit(1); - } +/* ------------------------------------------------------------------------- */ +/* String operations */ +/* ------------------------------------------------------------------------- */ + +static char* fr_strdup(const char* s) { + if (!s) s = ""; + size_t len = (size_t)strlen(s); + char* out = (char*)malloc(len + 1); + if (!out) return NULL; + memcpy(out, s, len + 1); + return out; } -// ============================================================================ -// Global variables -// ============================================================================ +char* runtime_str_concat(const char* a, const char* b) { + if (!a) a = ""; + if (!b) b = ""; + size_t la = (size_t)strlen(a); + size_t lb = (size_t)strlen(b); + char* result = (char*)malloc(la + lb + 1); + if (!result) return NULL; + memcpy(result, a, la); + memcpy(result + la, b, lb + 1); + return result; +} + +int64_t runtime_str_len(const char* str) { return strlen(str); } + +int64_t runtime_str_contains(const char* haystack, const char* needle) { + return strstr(haystack, needle) != NULL ? 1 : 0; +} + +int64_t runtime_str_eq(const char* a, const char* b) { + if (!a || !b) return a == b; + return strcmp(a, b) == 0; +} + +char* runtime_str_get_char(const char* str, int64_t index) { + int64_t len = strlen(str); + if (index < 0) index += len; + if (index < 0 || index >= len) return fr_strdup(""); + char* result = (char*)malloc(2); + if (!result) return NULL; + result[0] = str[index]; + result[1] = '\0'; + return result; +} + +char* runtime_str_upper(const char* str) { + size_t len = (size_t)strlen(str); + char* result = (char*)malloc(len + 1); + if (!result) return NULL; + for (size_t i = 0; i < len; i++) { + char c = str[i]; + result[i] = (c >= 'a' && c <= 'z') ? (char)(c - 'a' + 'A') : c; + } + result[len] = '\0'; + return result; +} + +char* runtime_str_lower(const char* str) { + size_t len = (size_t)strlen(str); + char* result = (char*)malloc(len + 1); + if (!result) return NULL; + for (size_t i = 0; i < len; i++) { + char c = str[i]; + result[i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c; + } + result[len] = '\0'; + return result; +} + +static bool fr_isspace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f'; +} + +char* runtime_str_strip(const char* str) { + const char* start = str; + while (*start && fr_isspace(*start)) start++; + const char* end = start + strlen(start); + while (end > start && fr_isspace(end[-1])) end--; + size_t len = (size_t)(end - start); + char* result = (char*)malloc(len + 1); + if (!result) return NULL; + memcpy(result, start, len); + result[len] = '\0'; + return result; +} + +RuntimeList* runtime_list_new(void); /* forward */ + +char* runtime_str_join(RuntimeList* list, const char* delim) { + if (!list || list->length == 0) return fr_strdup(""); + if (!delim) delim = ""; + size_t delim_len = (size_t)strlen(delim); + size_t total = 0; + for (int64_t i = 0; i < list->length; i++) { + const char* s = (const char*)list->items[i]; + total += (size_t)strlen(s); + if (i + 1 < list->length) total += delim_len; + } + char* result = (char*)malloc(total + 1); + if (!result) return NULL; + char* out = result; + for (int64_t i = 0; i < list->length; i++) { + const char* s = (const char*)list->items[i]; + size_t len = (size_t)strlen(s); + memcpy(out, s, len); + out += len; + if (i + 1 < list->length) { + memcpy(out, delim, delim_len); + out += delim_len; + } + } + *out = '\0'; + return result; +} + +char* runtime_str_replace(const char* str, const char* old, const char* repl) { + if (!old || !repl || !*old) return fr_strdup(str); + size_t old_len = (size_t)strlen(old); + size_t repl_len = (size_t)strlen(repl); + size_t count = 0; + const char* p = str; + while ((p = strstr(p, old)) != NULL) { count++; p += old_len; } + if (count == 0) return fr_strdup(str); + + size_t src_len = (size_t)strlen(str); + size_t out_len = src_len + count * (repl_len - old_len); + char* result = (char*)malloc(out_len + 1); + if (!result) return NULL; + char* out = result; + const char* src = str; + while ((p = strstr(src, old)) != NULL) { + size_t chunk = (size_t)(p - src); + memcpy(out, src, chunk); + out += chunk; + memcpy(out, repl, repl_len); + out += repl_len; + src = p + old_len; + } + strcpy(out, src); + return result; +} + +RuntimeList* runtime_str_split(const char* str, const char* delim) { + RuntimeList* list = runtime_list_new(); + list->elem_type = 1; + if (!delim || !*delim) { + runtime_list_append_int(list, (int64_t)fr_strdup(str)); + return list; + } + size_t delim_len = (size_t)strlen(delim); + const char* start = str; + const char* p = str; + while (*p) { + if (strncmp(p, delim, delim_len) == 0) { + size_t len = (size_t)(p - start); + char* token = (char*)malloc(len + 1); + if (token) { + memcpy(token, start, len); + token[len] = '\0'; + runtime_list_append_int(list, (int64_t)token); + } + p += delim_len; + start = p; + } else { + p++; + } + } + runtime_list_append_int(list, (int64_t)fr_strdup(start)); + return list; +} + +char* runtime_str_repeat(const char* str, int64_t count) { + if (count <= 0) return fr_strdup(""); + size_t len = (size_t)strlen(str); + size_t max = (size_t)-1; + if (len != 0 && (size_t)count > (max - 1) / len) return NULL; + char* result = (char*)malloc(len * (size_t)count + 1); + if (!result) return NULL; + char* out = result; + for (int64_t i = 0; i < count; i++) { + memcpy(out, str, len); + out += len; + } + *out = '\0'; + return result; +} + +char* runtime_str_encode(const char* str) { return fr_strdup(str); } +char* runtime_str_decode(const char* bytes) { return fr_strdup(bytes); } + +int64_t runtime_str_to_int(const char* str) { + if (!str) return 0; + int64_t sign = 1; + if (*str == '-') { sign = -1; str++; } + else if (*str == '+') str++; + int64_t value = 0; + bool any = false; + while (*str >= '0' && *str <= '9') { + any = true; + value = value * 10 + (*str - '0'); + str++; + } + return any ? value * sign : 0; +} + +double runtime_str_to_float(const char* str) { + if (!str) return 0.0; + double sign = 1.0; + if (*str == '-') { sign = -1.0; str++; } + else if (*str == '+') str++; + double value = 0.0; + bool any = false; + while (*str >= '0' && *str <= '9') { + any = true; + value = value * 10.0 + (double)(*str - '0'); + str++; + } + if (*str == '.') { + str++; + double place = 0.1; + while (*str >= '0' && *str <= '9') { + any = true; + value += (double)(*str - '0') * place; + place *= 0.1; + str++; + } + } + return any ? value * sign : 0.0; +} + +/* ------------------------------------------------------------------------- */ +/* List operations */ +/* ------------------------------------------------------------------------- */ + +RuntimeList* runtime_list_new(void) { + RuntimeList* list = (RuntimeList*)fr_obj_alloc(sizeof(RuntimeList), FR_OBJ_LIST, 0); + if (!list) return NULL; + list->capacity = 8; + list->length = 0; + list->elem_type = -1; + list->items = (int64_t*)malloc((size_t)list->capacity * sizeof(int64_t)); + if (!list->items) { + free(list); + return NULL; + } + return list; +} + +RuntimeList* runtime_list_new_typed(int64_t elem_type) { + RuntimeList* list = runtime_list_new(); + if (list) list->elem_type = (int)elem_type; + return list; +} + +static void fr_list_append_raw(RuntimeList* list, int64_t value, bool retain) { + if (!list) return; + if (retain) { + runtime_retain((void*)value); + runtime_scope_forget((void*)value); + } + if (list->length >= list->capacity) { + int64_t new_cap = list->capacity ? list->capacity * 2 : 8; + int64_t* items = (int64_t*)realloc( + list->items, (size_t)new_cap * sizeof(int64_t)); + if (!items) return; + list->items = items; + list->capacity = new_cap; + } + list->items[list->length++] = value; +} + +void runtime_list_append_int(RuntimeList* list, int64_t value) { + fr_list_append_raw(list, value, list && list->elem_type == 4); +} + +void runtime_list_append_managed(RuntimeList* list, int64_t value) { + if (!list) return; + if (list->elem_type < 0 || list->elem_type == 0) list->elem_type = 4; + fr_list_append_raw(list, value, true); +} + +int64_t runtime_list_get_int(RuntimeList* list, int64_t index) { + if (!list) return 0; + if (index < 0) index += list->length; + if (index < 0 || index >= list->length) return 0; + return list->items[index]; +} + +void runtime_list_set_int(RuntimeList* list, int64_t index, int64_t value) { + if (!list) return; + if (index < 0) index += list->length; + if (index < 0 || index >= list->length) return; + if (list->elem_type == 4) { + if (list->items[index] == value) return; + runtime_retain((void*)value); + runtime_scope_forget((void*)value); + runtime_release((void*)list->items[index]); + } + list->items[index] = value; +} + +void runtime_list_set_managed(RuntimeList* list, int64_t index, int64_t value) { + if (!list) return; + if (list->elem_type < 0 || list->elem_type == 0) list->elem_type = 4; + runtime_list_set_int(list, index, value); +} + +int64_t runtime_list_len(RuntimeList* list) { return list ? list->length : 0; } + +void* runtime_list_elem_addr(RuntimeList* list, int64_t index) { + if (!list) runtime_error_at("Index error: null list pointer", 0); + int64_t original = index; + if (index < 0) index += list->length; + if (index < 0 || index >= list->length) { + char msg[256]; + char index_str[32]; + char length_str[32]; + fr_int64_to_buf(original, index_str); + fr_int64_to_buf(list->length, length_str); + strcpy(msg, "Index error: list index out of range: "); + strcat(msg, index_str); + strcat(msg, " (length: "); + strcat(msg, length_str); + strcat(msg, ")"); + runtime_error_at(msg, 0); + } + return (void*)&list->items[index]; +} + +int64_t runtime_list_pop(RuntimeList* list) { + if (!list || list->length == 0) return 0; + return list->items[--list->length]; +} + +static void fr_int64_to_buf(int64_t value, char* buffer) { + char* p = buffer + 31; + *p = '\0'; + uint64_t n = value < 0 ? (uint64_t)(-(value + 1)) + 1 : (uint64_t)value; + if (!n) { + *--p = '0'; + } else { + while (n) { *--p = (char)('0' + (n % 10)); n /= 10; } + if (value < 0) *--p = '-'; + } + while (*p) *buffer++ = *p++; + *buffer = '\0'; +} + +int64_t runtime_list_get_int_at(RuntimeList* list, int64_t index, int line) { + if (!list) { + runtime_error_at("Index error: null list pointer", line); + } + int64_t original = index; + if (index < 0) index += list->length; + if (index < 0 || index >= list->length) { + char msg[256]; + char index_str[32]; + char length_str[32]; + fr_int64_to_buf(original, index_str); + fr_int64_to_buf(list->length, length_str); + strcpy(msg, "Index error: list index out of range: "); + strcat(msg, index_str); + strcat(msg, " (length: "); + strcat(msg, length_str); + strcat(msg, ")"); + runtime_error_at(msg, line); + } + return list->items[index]; +} + +void runtime_list_set_int_at(RuntimeList* list, int64_t index, int64_t value, + int line) { + int64_t original = index; + if (index < 0) index += list->length; + if (index < 0 || index >= list->length) { + char msg[256]; + char index_str[32]; + char length_str[32]; + fr_int64_to_buf(original, index_str); + fr_int64_to_buf(list->length, length_str); + strcpy(msg, "Index error: list index out of range: "); + strcat(msg, index_str); + strcat(msg, " (length: "); + strcat(msg, length_str); + strcat(msg, ")"); + runtime_error_at(msg, line); + } + if (list->elem_type == 4) { + if (list->items[index] == value) return; + runtime_retain((void*)value); + runtime_scope_forget((void*)value); + runtime_release((void*)list->items[index]); + } + list->items[index] = value; +} + +void runtime_list_set_managed_at(RuntimeList* list, int64_t index, int64_t value, + int line) { + if (!list) return; + if (list->elem_type < 0 || list->elem_type == 0) list->elem_type = 4; + runtime_list_set_int_at(list, index, value, line); +} + +RuntimeList* runtime_list_from_array(int64_t* values, int64_t count) { + RuntimeList* list = runtime_list_new(); + if (!list) return NULL; + list->capacity = count > 0 ? count : 8; + list->length = count; + list->items = (int64_t*)malloc((size_t)list->capacity * sizeof(int64_t)); + if (!list->items) return NULL; + if (count > 0) memcpy(list->items, values, (size_t)count * sizeof(int64_t)); + return list; +} + +RuntimeList* runtime_list_from_array_typed(int64_t* values, int64_t count, + int64_t elem_type) { + RuntimeList* list = runtime_list_from_array(values, count); + if (list) { + list->elem_type = (int)elem_type; + if (elem_type == 4) { + for (int64_t i = 0; i < count; i++) { + runtime_retain((void*)values[i]); + runtime_scope_forget((void*)values[i]); + } + } + } + return list; +} + +static bool runtime_list_contains_value(RuntimeList* list, int64_t value) { + if (!list) return false; + for (int64_t i = 0; i < list->length; i++) { + if (list->elem_type == 1) { + if (strcmp((const char*)list->items[i], (const char*)value) == 0) + return true; + } else if (list->items[i] == value) { + return true; + } + } + return false; +} + +int64_t runtime_contains(RuntimeList* list, int64_t value) { + return runtime_list_contains_value(list, value) ? 1 : 0; +} + +void runtime_list_free(RuntimeList* list) { + if (!list) return; + free(list->items); + free(list); +} + +char* runtime_list_to_str(RuntimeList* list) { + if (!list) return fr_strdup("[]"); + size_t capacity = 256; + for (int64_t i = 0; i < list->length; i++) { + if (list->elem_type == 1) capacity += (size_t)strlen((const char*)list->items[i]) + 2; + else capacity += 32; + } + char* result = (char*)malloc(capacity); + if (!result) return NULL; + char* out = result; + *out++ = '['; + for (int64_t i = 0; i < list->length; i++) { + if (i) { *out++ = ','; *out++ = ' '; } + if (list->elem_type == 1) { + const char* s = (const char*)list->items[i]; + size_t len = (size_t)strlen(s); + memcpy(out, s, len); + out += len; + } else if (list->elem_type == 2) { + union { int64_t i; double d; } pun; + pun.i = list->items[i]; + char* s = runtime_float_to_str(pun.d); + if (s) { + size_t len = (size_t)strlen(s); + memcpy(out, s, len); + out += len; + free(s); + } + } else if (list->elem_type == 3) { + const char* s = list->items[i] ? "true" : "false"; + size_t len = (size_t)strlen(s); + memcpy(out, s, len); + out += len; + } else { + char buf[32]; + fr_int64_to_buf(list->items[i], buf); + size_t len = (size_t)strlen(buf); + memcpy(out, buf, len); + out += len; + } + } + *out++ = ']'; + *out = '\0'; + return result; +} + +/* ------------------------------------------------------------------------- */ +/* Set operations */ +/* ------------------------------------------------------------------------- */ + +typedef struct { + int64_t* items; + int64_t length; + int64_t capacity; + int elem_type; +} RuntimeSet; + +typedef struct { + int64_t* keys; + int64_t* values; + int64_t length; + int64_t capacity; + int key_type; /* -1 unknown, 0 int, 1 string */ +} RuntimeDict; + +RuntimeDict* runtime_dict_new(void) { + RuntimeDict* dict = (RuntimeDict*)fr_obj_alloc(sizeof(RuntimeDict), FR_OBJ_DICT, 0); + if (!dict) return NULL; + dict->capacity = 8; + dict->length = 0; + dict->key_type = -1; + dict->keys = (int64_t*)malloc((size_t)dict->capacity * sizeof(int64_t)); + dict->values = (int64_t*)malloc((size_t)dict->capacity * sizeof(int64_t)); + if (!dict->keys || !dict->values) return NULL; + return dict; +} + +static bool runtime_dict_key_eq(RuntimeDict* dict, int64_t a, int64_t b) { + if (dict->key_type == 1) return strcmp((const char*)a, (const char*)b) == 0; + return a == b; +} + +void runtime_dict_set(RuntimeDict* dict, int64_t key, int64_t value) { + if (!dict) return; + if (dict->key_type == -1) dict->key_type = key > 0x100000 ? 1 : 0; + for (int64_t i = 0; i < dict->length; i++) { + if (runtime_dict_key_eq(dict, dict->keys[i], key)) { + dict->values[i] = value; + return; + } + } + if (dict->length >= dict->capacity) { + int64_t cap = dict->capacity * 2; + int64_t* keys = (int64_t*)realloc(dict->keys, (size_t)cap * sizeof(int64_t)); + int64_t* values = (int64_t*)realloc(dict->values, (size_t)cap * sizeof(int64_t)); + if (!keys || !values) return; + dict->keys = keys; + dict->values = values; + dict->capacity = cap; + } + dict->keys[dict->length] = key; + dict->values[dict->length] = value; + dict->length++; +} + +int64_t runtime_dict_get(RuntimeDict* dict, int64_t key) { + if (!dict) return 0; + for (int64_t i = 0; i < dict->length; i++) { + if (runtime_dict_key_eq(dict, dict->keys[i], key)) return dict->values[i]; + } + return 0; +} + +int64_t runtime_dict_contains(RuntimeDict* dict, int64_t key) { + if (!dict) return 0; + for (int64_t i = 0; i < dict->length; i++) { + if (runtime_dict_key_eq(dict, dict->keys[i], key)) return 1; + } + return 0; +} + +int64_t runtime_dict_len(RuntimeDict* dict) { return dict ? dict->length : 0; } + +void runtime_dict_set_typed(RuntimeDict* dict, int64_t key, int64_t value, + int64_t key_type) { + if (!dict) return; + if (dict->key_type == -1) dict->key_type = (int)key_type; + runtime_dict_set(dict, key, value); +} + +int64_t runtime_dict_get_typed(RuntimeDict* dict, int64_t key, int64_t key_type) { + if (!dict) return 0; + if (dict->key_type == -1) dict->key_type = (int)key_type; + return runtime_dict_get(dict, key); +} + +int64_t runtime_dict_contains_typed(RuntimeDict* dict, int64_t key, + int64_t key_type) { + if (!dict) return 0; + if (dict->key_type == -1) dict->key_type = (int)key_type; + return runtime_dict_contains(dict, key); +} + +RuntimeSet* runtime_set_new(void) { + RuntimeSet* set = (RuntimeSet*)fr_obj_alloc(sizeof(RuntimeSet), FR_OBJ_SET, 0); + if (!set) return NULL; + set->capacity = 16; + set->length = 0; + set->elem_type = -1; + set->items = (int64_t*)malloc((size_t)set->capacity * sizeof(int64_t)); + if (!set->items) return NULL; + return set; +} + +static bool runtime_set_equal(RuntimeSet* set, int64_t a, int64_t b) { + if (set->elem_type == 1) return strcmp((const char*)a, (const char*)b) == 0; + return a == b; +} + +void runtime_set_add(RuntimeSet* set, int64_t value) { + if (!set) return; + if (set->elem_type == -1) set->elem_type = value > 0x100000 ? 1 : 0; + for (int64_t i = 0; i < set->length; i++) { + if (runtime_set_equal(set, set->items[i], value)) return; + } + if (set->elem_type == 4) { + runtime_retain((void*)value); + runtime_scope_forget((void*)value); + } + if (set->length >= set->capacity) { + int64_t new_cap = set->capacity * 2; + int64_t* items = (int64_t*)realloc( + set->items, (size_t)new_cap * sizeof(int64_t)); + if (!items) return; + set->items = items; + set->capacity = new_cap; + } + set->items[set->length++] = value; +} + +void runtime_set_add_typed(RuntimeSet* set, int64_t value, int elem_type) { + if (!set) return; + if (set->elem_type == -1) set->elem_type = elem_type; + runtime_set_add(set, value); +} + +void runtime_set_remove(RuntimeSet* set, int64_t value) { + if (!set) return; + for (int64_t i = 0; i < set->length; i++) { + if (runtime_set_equal(set, set->items[i], value)) { + if (set->elem_type == 4) runtime_release((void*)set->items[i]); + for (int64_t j = i; j + 1 < set->length; j++) + set->items[j] = set->items[j + 1]; + set->length--; + return; + } + } +} + +int64_t runtime_set_contains(RuntimeSet* set, int64_t value) { + if (!set) return 0; + for (int64_t i = 0; i < set->length; i++) { + if (runtime_set_equal(set, set->items[i], value)) return 1; + } + return 0; +} + +int64_t runtime_set_len(RuntimeSet* set) { return set ? set->length : 0; } + +void runtime_set_free(RuntimeSet* set) { + if (!set) return; + free(set->items); + free(set); +} + +char* runtime_set_to_str(RuntimeSet* set) { + if (!set) return fr_strdup("{}"); + size_t capacity = 256; + for (int64_t i = 0; i < set->length; i++) capacity += 40; + char* result = (char*)malloc(capacity); + if (!result) return NULL; + char* out = result; + *out++ = '{'; + for (int64_t i = 0; i < set->length; i++) { + if (i) { *out++ = ','; *out++ = ' '; } + if (set->elem_type == 1) { + const char* s = (const char*)set->items[i]; + size_t len = (size_t)strlen(s); + memcpy(out, s, len); + out += len; + } else { + char buf[32]; + fr_int64_to_buf(set->items[i], buf); + size_t len = (size_t)strlen(buf); + memcpy(out, buf, len); + out += len; + } + } + *out++ = '}'; + *out = '\0'; + return result; +} + +/* ------------------------------------------------------------------------- */ +/* Error handling / exceptions */ +/* ------------------------------------------------------------------------- */ + +void int64_to_str(int64_t value, char* buffer); + +static void fr_append_bounded(char* dst, const char* src, size_t cap) { + if (!dst || !src || cap == 0) return; + size_t used = strlen(dst); + if (used >= cap - 1) return; + size_t room = cap - 1 - used; + size_t n = strlen(src); + if (n > room) n = room; + memcpy(dst + used, src, n); + dst[used + n] = '\0'; +} + +#define MAX_EXCEPTION_HANDLERS 256 +typedef struct { + char* exc_type; + uint64_t jump_buffer[8]; + int64_t scope_depth; +} RuntimeExceptionHandler; + +static RuntimeExceptionHandler exception_handlers[MAX_EXCEPTION_HANDLERS]; +static int exception_handler_count = 0; + +void runtime_error_at(const char* message, int line) { + const char* exc_type = "RuntimeError"; + if (strstr(message, "division by zero")) exc_type = "ZeroDivisionError"; + else if (strstr(message, "Index error")) exc_type = "IndexError"; + + for (int i = exception_handler_count - 1; i >= 0; i--) { + RuntimeExceptionHandler* handler = &exception_handlers[i]; + if (handler->exc_type[0] == '\0' || + strcmp(handler->exc_type, exc_type) == 0) { + runtime_scope_unwind(handler->scope_depth); + longjmp(handler->jump_buffer, 1); + } + } + + char full[1024]; + char line_str[32]; + fr_int64_to_buf(line, line_str); + strcpy(full, "?"); + fr_append_bounded(full, line_str, sizeof(full)); + fr_append_bounded(full, ",0:", sizeof(full)); + fr_append_bounded(full, message, sizeof(full)); + fr_append_bounded(full, "\n", sizeof(full)); + fr_fputs(full, 2); + fr_exit(1); +} + +void runtime_exception_init(void) { exception_handler_count = 0; } + +void* runtime_exception_push(const char* exc_type) { + if (exception_handler_count >= MAX_EXCEPTION_HANDLERS) { + fr_fputs("Runtime error: exception handler depth exceeded\n", 2); + fr_exit(1); + } + RuntimeExceptionHandler* handler = &exception_handlers[exception_handler_count]; + handler->exc_type = (char*)(exc_type ? exc_type : ""); + handler->scope_depth = fr_scope_depth; + return (void*)(int64_t)exception_handler_count++; +} + +void* runtime_exception_get_jump_buffer(void) { + if (exception_handler_count == 0) return NULL; + return &exception_handlers[exception_handler_count - 1].jump_buffer; +} + +void runtime_exception_pop(void) { + if (exception_handler_count > 0) exception_handler_count--; +} + +__attribute__((noinline, optimize("no-omit-frame-pointer"))) +void runtime_exception_raise(const char* exc_type, const char* message) { + if (!exc_type) exc_type = ""; + if (!message) message = ""; + for (int i = exception_handler_count - 1; i >= 0; i--) { + RuntimeExceptionHandler* handler = &exception_handlers[i]; + if (handler->exc_type[0] == '\0' || + strcmp(handler->exc_type, exc_type) == 0) { + runtime_scope_unwind(handler->scope_depth); + longjmp(handler->jump_buffer, 1); + } + } + char full[1024]; + strcpy(full, "["); + fr_append_bounded(full, exc_type, sizeof(full)); + fr_append_bounded(full, "] ", sizeof(full)); + fr_append_bounded(full, message, sizeof(full)); + fr_append_bounded(full, "\n", sizeof(full)); + fr_fputs(full, 2); + fr_exit(1); +} + +__attribute__((noinline, optimize("no-omit-frame-pointer"))) +void runtime_exception_raise_at(const char* exc_type, const char* message, + int line) { + for (int i = exception_handler_count - 1; i >= 0; i--) { + RuntimeExceptionHandler* handler = &exception_handlers[i]; + if (handler->exc_type[0] == '\0' || + strcmp(handler->exc_type, exc_type) == 0) { + runtime_scope_unwind(handler->scope_depth); + longjmp(handler->jump_buffer, 1); + } + } + char full[1024]; + char line_str[32]; + fr_int64_to_buf(line, line_str); + strcpy(full, "?"); + fr_append_bounded(full, line_str, sizeof(full)); + fr_append_bounded(full, ",0:[", sizeof(full)); + fr_append_bounded(full, exc_type, sizeof(full)); + fr_append_bounded(full, "] ", sizeof(full)); + fr_append_bounded(full, message, sizeof(full)); + fr_append_bounded(full, "\n", sizeof(full)); + fr_fputs(full, 2); + fr_exit(1); +} + +void runtime_check_div_zero_i64_at(int64_t divisor, int line) { + if (divisor == 0) runtime_error_at("integer division by zero", line); +} + +void runtime_check_div_zero_f64_at(double divisor, int line) { + if (divisor == 0.0) runtime_error_at("float division by zero", line); +} + +void runtime_check_div_zero_i64(int64_t divisor) { + if (divisor == 0) runtime_exception_raise("ZeroDivisionError", + "integer division by zero"); +} + +void runtime_check_div_zero_f64(double divisor) { + if (divisor == 0.0) runtime_exception_raise("ZeroDivisionError", + "float division by zero"); +} + +void runtime_assert(bool condition, const char* message) { + if (!condition) { + if (message && message[0] != '\0') { + fr_fputs(message, 1); + fr_putchar('\n'); + } else { + fr_fputs("Assertion failed\n", 1); + } + fr_exit(1); + } +} + +/* ------------------------------------------------------------------------- */ +/* Process / time */ +/* ------------------------------------------------------------------------- */ + +int64_t runtime_fork(void) { + int64_t pid = syscall1(SYS_FORK, 0); + return pid; +} + +int64_t runtime_wait(int64_t pid) { + int status = 0; + int64_t result = syscall6(SYS_WAIT4, pid, (int64_t)&status, 0, 0, 0, 0); + if (result < 0) return -1; + return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : status; +} + +void runtime_sleep(double seconds) { + if (seconds <= 0.0) return; + int64_t sec = (int64_t)seconds; + int64_t nsec = (int64_t)((seconds - (double)sec) * 1000000000.0); + int64_t ts[2] = {sec, nsec}; + syscall3(SYS_NANOSLEEP, (int64_t)ts, 0, 0); +} + +void runtime_exit(int64_t status) { fr_exit((int)status); } + +/* ------------------------------------------------------------------------- */ +/* File I/O */ +/* ------------------------------------------------------------------------- */ + +static int fr_open_flags(const char* mode) { + bool plus = false; + if (!mode) return O_RDONLY; + for (const char* p = mode; *p; p++) if (*p == '+') plus = true; + switch (mode[0]) { + case 'w': + return O_CREAT | O_TRUNC | (plus ? O_RDWR : O_WRONLY); + case 'a': + return O_CREAT | O_APPEND | (plus ? O_RDWR : O_WRONLY); + case 'r': + default: + return plus ? O_RDWR : O_RDONLY; + } +} + +int64_t runtime_fopen(const char* path, const char* mode) { + int flags = fr_open_flags(mode); +#ifdef FR_TARGET_FR_OS + return syscall3(SYS_OPEN, (int64_t)path, flags, 0666); +#else + int64_t fd = syscall6(SYS_OPENAT, AT_FDCWD, (int64_t)path, flags, 0666, 0, 0); + return fd; +#endif +} + +int64_t runtime_fwrite(int64_t fd, const char* data) { + if (fd < 0 || !data) return 0; + size_t len = (size_t)strlen(data); + size_t off = 0; + while (off < len) { + int64_t n = syscall3(SYS_WRITE, fd, (int64_t)(data + off), + (int64_t)(len - off)); + if (n <= 0) break; + off += (size_t)n; + } + return (int64_t)off; +} + +char* runtime_fread(int64_t fd, int64_t size) { + if (fd < 0) return fr_strdup(""); + if (size < 0) { + int64_t cur = syscall3(SYS_LSEEK, fd, 0, SEEK_CUR); + int64_t end = syscall3(SYS_LSEEK, fd, 0, SEEK_END); + syscall3(SYS_LSEEK, fd, cur, SEEK_SET); + if (end < cur) return fr_strdup(""); + size = end - cur; + } + char* buffer = (char*)malloc((size_t)size + 1); + if (!buffer) return NULL; + size_t got = 0; + while (got < (size_t)size) { + int64_t n = syscall3(SYS_READ, fd, (int64_t)(buffer + got), + (int64_t)((size_t)size - got)); + if (n <= 0) break; + got += (size_t)n; + } + buffer[got] = '\0'; + return buffer; +} + +void runtime_fclose(int64_t fd) { + if (fd >= 0) syscall1(SYS_CLOSE, fd); +} + +/* ------------------------------------------------------------------------- */ +/* Input */ +/* ------------------------------------------------------------------------- */ + +char* runtime_input(void) { + size_t capacity = 128; + size_t length = 0; + char* buffer = (char*)malloc(capacity); + if (!buffer) return NULL; + while (1) { + char c; + int64_t n = syscall3(SYS_READ, 0, (int64_t)&c, 1); + if (n <= 0) break; + if (c == '\n') break; + if (c == '\r') continue; + if (length + 1 >= capacity) { + capacity *= 2; + char* grown = (char*)realloc(buffer, capacity); + if (!grown) break; + buffer = grown; + } + buffer[length++] = c; + } + buffer[length] = '\0'; + return buffer; +} + +/* ------------------------------------------------------------------------- */ +/* Sockets (Linux syscalls) */ +/* ------------------------------------------------------------------------- */ + +#ifdef FR_TARGET_FR_OS +/* Socket domain per docs/src/syscalls (the current Fr OS header only assigns + * SYS_socket; the remaining numbers are the documented goal ABI). */ +#define SYS_SOCKET 160 +#define SYS_BIND 161 +#define SYS_CONNECT 162 +#define SYS_LISTEN 163 +#define SYS_ACCEPT 164 +#define SYS_SENDTO 165 +#define SYS_RECVFROM 166 +#define SYS_SETSOCKOPT 167 +#define SYS_SHUTDOWN 206 /* current Fr OS uapi; docs goal says 168 */ +#else +#define SYS_SOCKET 41 +#define SYS_CONNECT 42 +#define SYS_ACCEPT 43 +#define SYS_SENDTO 44 +#define SYS_RECVFROM 45 +#define SYS_SHUTDOWN 48 +#define SYS_BIND 49 +#define SYS_LISTEN 50 +#define SYS_SETSOCKOPT 54 +#endif + +#define AF_INET_ 2 +#define SOCK_STREAM_ 1 +#define SOCK_DGRAM_ 2 +#define SOL_SOCKET_ 1 +#define SO_REUSEADDR_ 2 +#define SO_KEEPALIVE_ 9 +#define SO_BROADCAST_ 6 +#define SO_RCVBUF_ 8 +#define SO_SNDBUF_ 7 +#define IPPROTO_TCP_ 6 +#define IPPROTO_IP_ 0 +#define MSG_NOSIGNAL_ 0x4000 + +typedef struct { + uint16_t family; + uint16_t port; /* network byte order */ + uint32_t addr; /* network byte order */ + uint8_t zero[8]; +} FrSockaddrIn; + +static bool fr_streq_ci(const char* a, const char* b) { + if (!a || !b) return false; + while (*a && *b) { + char ca = *a, cb = *b; + if (ca >= 'A' && ca <= 'Z') ca = (char)(ca - 'A' + 'a'); + if (cb >= 'A' && cb <= 'Z') cb = (char)(cb - 'A' + 'a'); + if (ca != cb) return false; + a++; b++; + } + return *a == *b; +} + +static uint16_t fr_htons(uint16_t v) { + return (uint16_t)((v << 8) | (v >> 8)); +} + +static int fr_parse_ipv4(const char* host, uint32_t* out) { + if (!host || !*host) { *out = 0; return 0; } + if (fr_streq_ci(host, "localhost")) { + *out = fr_htons((uint16_t)(127 | (0 << 8))); /* 127.0.0.1 bytes */ + *out = ((uint32_t)127) | ((uint32_t)0 << 8) | ((uint32_t)0 << 16) | ((uint32_t)1 << 24); + return 0; + } + uint32_t parts[4] = {0, 0, 0, 0}; + int part = 0; + for (const char* p = host; *p; p++) { + if (*p == '.') { + part++; + if (part > 3) return -1; + } else if (*p >= '0' && *p <= '9') { + parts[part] = parts[part] * 10 + (uint32_t)(*p - '0'); + if (parts[part] > 255) return -1; + } else { + return -1; + } + } + if (part != 3) return -1; + *out = parts[0] | (parts[1] << 8) | (parts[2] << 16) | (parts[3] << 24); + return 0; +} + +int64_t runtime_socket_create(const char* family, const char* type) { + int fam = AF_INET_; + if (family && fr_streq_ci(family, "inet6")) fam = 10; + else if (family && fr_streq_ci(family, "unix")) fam = 1; + int typ = SOCK_STREAM_; + if (type && fr_streq_ci(type, "dgram")) typ = SOCK_DGRAM_; + else if (type && fr_streq_ci(type, "raw")) typ = 3; + + int64_t fd = syscall3(SYS_SOCKET, fam, typ, 0); + if (fd >= 0) { + int one = 1; + syscall6(SYS_SETSOCKOPT, fd, SOL_SOCKET_, SO_REUSEADDR_, + (int64_t)&one, sizeof(one), 0); + } + return fd; +} + +int64_t runtime_socket_connect(int64_t fd, const char* host, int64_t port) { + FrSockaddrIn addr; + memset(&addr, 0, sizeof(addr)); + addr.family = AF_INET_; + addr.port = fr_htons((uint16_t)port); + if (fr_parse_ipv4(host, &addr.addr) != 0) return -1; + return syscall3(SYS_CONNECT, fd, (int64_t)&addr, sizeof(addr)); +} + +int64_t runtime_socket_bind(int64_t fd, const char* host, int64_t port) { + FrSockaddrIn addr; + memset(&addr, 0, sizeof(addr)); + addr.family = AF_INET_; + addr.port = fr_htons((uint16_t)port); + if (fr_parse_ipv4(host, &addr.addr) != 0) return -1; + return syscall3(SYS_BIND, fd, (int64_t)&addr, sizeof(addr)); +} + +int64_t runtime_socket_listen(int64_t fd, int64_t backlog) { + return syscall3(SYS_LISTEN, fd, backlog, 0); +} + +int64_t runtime_socket_accept(int64_t fd) { + return syscall3(SYS_ACCEPT, fd, 0, 0); +} + +int64_t runtime_socket_send(int64_t fd, const char* data) { + if (!data) return 0; + size_t len = (size_t)strlen(data); + size_t sent = 0; + while (sent < len) { + int64_t n = syscall6(SYS_SENDTO, fd, (int64_t)(data + sent), + (int64_t)(len - sent), MSG_NOSIGNAL_, 0, 0); + if (n <= 0) break; + sent += (size_t)n; + } + return (int64_t)sent; +} + +int64_t runtime_socket_sendall(int64_t fd, const char* data) { + return runtime_socket_send(fd, data); +} + +char* runtime_socket_recv(int64_t fd, int64_t size) { + if (fd < 0) return fr_strdup(""); + if (size <= 0) size = 4096; + char* buffer = (char*)malloc((size_t)size + 1); + if (!buffer) return fr_strdup(""); + int64_t n = syscall6(SYS_RECVFROM, fd, (int64_t)buffer, size, 0, 0, 0); + if (n < 0) n = 0; + buffer[n] = '\0'; + return buffer; +} + +int64_t runtime_socket_close(int64_t fd) { + if (fd < 0) return -1; + return syscall1(SYS_CLOSE, fd); +} + +int64_t runtime_socket_setsockopt(int64_t fd, const char* level, + const char* option, int64_t value) { + int lev = SOL_SOCKET_; + if (level && fr_streq_ci(level, "IPPROTO_TCP")) lev = IPPROTO_TCP_; + else if (level && fr_streq_ci(level, "IPPROTO_IP")) lev = IPPROTO_IP_; + + int opt = SO_REUSEADDR_; + if (option && fr_streq_ci(option, "SO_KEEPALIVE")) opt = SO_KEEPALIVE_; + else if (option && fr_streq_ci(option, "SO_BROADCAST")) opt = SO_BROADCAST_; + else if (option && fr_streq_ci(option, "SO_RCVBUF")) opt = SO_RCVBUF_; + else if (option && fr_streq_ci(option, "SO_SNDBUF")) opt = SO_SNDBUF_; + + int val = (int)value; + return syscall6(SYS_SETSOCKOPT, fd, lev, opt, (int64_t)&val, + sizeof(val), 0); +} + +int64_t runtime_socket_shutdown(int64_t fd, int64_t how) { + return syscall3(SYS_SHUTDOWN, fd, how, 0); +} + +/* ------------------------------------------------------------------------- */ +/* Filesystem helpers */ +/* ------------------------------------------------------------------------- */ + +#define FR_S_IFMT 0xF000 +#define FR_S_IFREG 0x8000 +#define FR_S_IFDIR 0x4000 +#define FR_O_DIRECTORY 0x10000 + +static int64_t fr_stat_path(const char* path, unsigned char* buf) { + memset(buf, 0, 160); +#ifdef FR_TARGET_FR_OS + int64_t fd = syscall3(SYS_OPEN, (int64_t)path, O_RDONLY, 0); + if (fd < 0) return fd; + int64_t rc = syscall3(SYS_FSTAT, fd, (int64_t)buf, 0); + syscall1(SYS_CLOSE, fd); + return rc; +#else + return syscall6(SYS_NEWFSTATAT, AT_FDCWD, (int64_t)path, + (int64_t)buf, 0, 0, 0); +#endif +} + +static int fr_stat_mode(const unsigned char* buf) { + int mode; +#ifdef FR_TARGET_FR_OS + memcpy(&mode, buf + 32, sizeof(mode)); /* struct kstat.mode */ +#else + memcpy(&mode, buf + 24, sizeof(mode)); +#endif + return mode; +} + +static int64_t fr_stat_size(const unsigned char* buf) { + int64_t size; +#ifdef FR_TARGET_FR_OS + memcpy(&size, buf + 0, sizeof(size)); /* struct kstat.size */ +#else + memcpy(&size, buf + 48, sizeof(size)); +#endif + return size; +} + +int64_t runtime_exists(const char* path) { + unsigned char buf[160]; + return fr_stat_path(path, buf) == 0 ? 1 : 0; +} + +int64_t runtime_isfile(const char* path) { + unsigned char buf[160]; + if (fr_stat_path(path, buf) != 0) return 0; + return (fr_stat_mode(buf) & FR_S_IFMT) == FR_S_IFREG ? 1 : 0; +} + +int64_t runtime_isdir(const char* path) { + unsigned char buf[160]; + if (fr_stat_path(path, buf) != 0) return 0; + return (fr_stat_mode(buf) & FR_S_IFMT) == FR_S_IFDIR ? 1 : 0; +} + +int64_t runtime_getsize(const char* path) { + unsigned char buf[160]; + if (fr_stat_path(path, buf) != 0) return -1; + return fr_stat_size(buf); +} + +char* runtime_getcwd(void) { +#ifdef FR_TARGET_FR_OS + return fr_strdup(""); +#else + char* buffer = (char*)malloc(4096); + if (!buffer) return fr_strdup(""); + int64_t n = syscall3(SYS_GETCWD, (int64_t)buffer, 4096, 0); + if (n <= 0) { + free(buffer); + return fr_strdup(""); + } + return buffer; +#endif +} + +int64_t runtime_chdir(const char* path) { +#ifdef FR_TARGET_FR_OS + (void)path; + return -1; +#else + return syscall1(SYS_CHDIR, (int64_t)path); +#endif +} + +int64_t runtime_mkdir(const char* path) { +#ifdef FR_TARGET_FR_OS + (void)path; + return -1; +#else + return syscall3(SYS_MKDIR, (int64_t)path, 0777, 0); +#endif +} + +int64_t runtime_rmdir(const char* path) { +#ifdef FR_TARGET_FR_OS + (void)path; + return -1; +#else + return syscall1(SYS_RMDIR, (int64_t)path); +#endif +} + +int64_t runtime_remove(const char* path) { +#ifdef FR_TARGET_FR_OS + (void)path; + return -1; +#else + return syscall1(SYS_UNLINK, (int64_t)path); +#endif +} + +int64_t runtime_rename(const char* old_path, const char* new_path) { +#ifdef FR_TARGET_FR_OS + (void)old_path; (void)new_path; + return -1; +#else + return syscall3(SYS_RENAME, (int64_t)old_path, (int64_t)new_path, 0); +#endif +} + +int64_t runtime_makedirs(const char* path) { +#ifdef FR_TARGET_FR_OS + (void)path; + return -1; +#else + if (!path || !*path) return -1; + char* copy = fr_strdup(path); + if (!copy) return -1; + size_t len = strlen(copy); + for (size_t i = 1; i <= len; i++) { + if (copy[i] == '/' || copy[i] == '\0') { + char saved = copy[i]; + copy[i] = '\0'; + if (copy[0]) runtime_mkdir(copy); + copy[i] = saved; + } + } + free(copy); + return 0; +#endif +} + +char* runtime_basename(const char* path) { + if (!path || !*path) return fr_strdup(""); + size_t len = strlen(path); + while (len > 1 && path[len - 1] == '/') len--; + size_t start = len; + while (start > 0 && path[start - 1] != '/') start--; + size_t out_len = len - start; + char* out = (char*)malloc(out_len + 1); + if (!out) return fr_strdup(""); + memcpy(out, path + start, out_len); + out[out_len] = '\0'; + return out; +} + +char* runtime_dirname(const char* path) { + if (!path || !*path) return fr_strdup("."); + size_t len = strlen(path); + while (len > 1 && path[len - 1] == '/') len--; + while (len > 0 && path[len - 1] != '/') len--; + if (len == 0) return fr_strdup("."); + while (len > 1 && path[len - 1] == '/') len--; + char* out = (char*)malloc(len + 1); + if (!out) return fr_strdup("."); + memcpy(out, path, len); + out[len] = '\0'; + return out; +} + +char* runtime_pathjoin(RuntimeList* parts) { + if (!parts || parts->length == 0) return fr_strdup(""); + size_t total = 1; + for (int64_t i = 0; i < parts->length; i++) { + total += (size_t)strlen((const char*)parts->items[i]) + 1; + } + char* out = (char*)malloc(total); + if (!out) return fr_strdup(""); + char* p = out; + for (int64_t i = 0; i < parts->length; i++) { + const char* part = (const char*)parts->items[i]; + if (!part || !*part) continue; + size_t len = (size_t)strlen(part); + int need_sep = (p != out && p[-1] != '/' && part[0] != '/'); + if (need_sep) *p++ = '/'; + if (p != out && p[-1] == '/' && part[0] == '/') part++; + len = (size_t)strlen(part); + memcpy(p, part, len); + p += len; + } + *p = '\0'; + return out; +} + +char* runtime_abspath(const char* path) { + if (!path || !*path) return runtime_getcwd(); + if (path[0] == '/') return fr_strdup(path); + char* cwd = runtime_getcwd(); + size_t a = (size_t)strlen(cwd); + size_t b = (size_t)strlen(path); + char* out = (char*)malloc(a + b + 2); + if (!out) { + free(cwd); + return fr_strdup(path); + } + memcpy(out, cwd, a); + out[a] = '/'; + memcpy(out + a + 1, path, b + 1); + free(cwd); + return out; +} + +RuntimeList* runtime_listdir(const char* path) { + RuntimeList* list = runtime_list_new(); + if (list) list->elem_type = 1; + if (!path) return list; + +#ifdef FR_TARGET_FR_OS + int64_t fd = syscall3(SYS_OPEN, (int64_t)path, O_RDONLY | FR_O_DIRECTORY, 0); +#else + int64_t fd = syscall6(SYS_OPENAT, AT_FDCWD, (int64_t)path, + O_RDONLY | FR_O_DIRECTORY, 0, 0, 0); +#endif + if (fd < 0) return list; + + char* buffer = (char*)malloc(32768); + if (!buffer) { + syscall1(SYS_CLOSE, fd); + return list; + } + for (;;) { + int64_t n = syscall3(SYS_GETDENTS64, fd, (int64_t)buffer, 32768); + if (n <= 0) break; + int64_t off = 0; + while (off < n) { + uint16_t reclen; + memcpy(&reclen, buffer + off + 16, sizeof(reclen)); + if (reclen == 0) break; +#ifdef FR_TARGET_FR_OS + uint8_t namlen; + memcpy(&namlen, buffer + off + 19, 1); + char namebuf[256]; + size_t copy_len = namlen < sizeof(namebuf) - 1 ? namlen : sizeof(namebuf) - 1; + memcpy(namebuf, buffer + off + 20, copy_len); + namebuf[copy_len] = '\0'; + const char* name = namebuf; +#else + const char* name = buffer + off + 19; +#endif + if (strcmp(name, ".") != 0 && strcmp(name, "..") != 0) { + runtime_list_append_int(list, (int64_t)fr_strdup(name)); + } + off += reclen; + } + } + free(buffer); + syscall1(SYS_CLOSE, fd); + return list; +} + +int64_t runtime_getpid(void) { + return syscall1(SYS_GETPID, 0); +} + +/* ------------------------------------------------------------------------- */ +/* Init / globals */ +/* ------------------------------------------------------------------------- */ + +void int64_to_str(int64_t value, char* buffer) { + fr_int64_to_buf(value, buffer); +} + +static void fr_release_fields(char* memory, int64_t mask) { + for (int i = 0; i < 64; i++) { + if (mask & ((int64_t)1 << i)) { + runtime_release(((void**)memory)[i]); + } + } +} + +void runtime_release(void* ptr) { + if (!fr_obj_valid(ptr)) return; + FrObjHeader* header = ((FrObjHeader*)ptr) - 1; + if (--header->refcount > 0) return; + header->magic = 0; + + if (header->kind == FR_OBJ_STRUCT) { + fr_release_fields((char*)(header + 1), header->aux); + } else if (header->kind == FR_OBJ_LIST) { + RuntimeList* list = (RuntimeList*)(header + 1); + if (list->elem_type == 4) { + for (int64_t i = 0; i < list->length; i++) { + runtime_release((void*)list->items[i]); + } + } + free(list->items); + } else if (header->kind == FR_OBJ_SET) { + RuntimeSet* set = (RuntimeSet*)(header + 1); + if (set->elem_type == 4) { + for (int64_t i = 0; i < set->length; i++) { + runtime_release((void*)set->items[i]); + } + } + free(set->items); + } else if (header->kind == FR_OBJ_DICT) { + RuntimeDict* dict = (RuntimeDict*)(header + 1); + free(dict->keys); + free(dict->values); + } + free(header); +} + +void runtime_init(void) { + init_heap(); + runtime_exception_init(); + fr_scope_depth = 0; + fr_scope_overflow = 0; +} -int64_t _fr_global_vars[256] = {0}; +int64_t _fr_global_vars[256] = {0}; \ No newline at end of file diff --git a/runtime/syscall.S b/runtime/syscall.S index a66b5e5..0696e32 100644 --- a/runtime/syscall.S +++ b/runtime/syscall.S @@ -29,3 +29,60 @@ syscall6: syscall ret .size syscall6, .-syscall6 + +/* Freestanding setjmp/longjmp used by the native exception machinery. */ +.global _setjmp +.type _setjmp,@function +_setjmp: + movq (%rsp), %rax + movq %rbx, 0(%rdi) + movq %rbp, 8(%rdi) + movq %r12, 16(%rdi) + movq %r13, 24(%rdi) + movq %r14, 32(%rdi) + movq %r15, 40(%rdi) + leaq 8(%rsp), %rcx + movq %rcx, 48(%rdi) + movq %rax, 56(%rdi) + xorl %eax, %eax + ret +.size _setjmp, .-_setjmp + +.type longjmp,@function +.global longjmp +longjmp: + movq 0(%rdi), %rbx + movq 8(%rdi), %rbp + movq 16(%rdi), %r12 + movq 24(%rdi), %r13 + movq 32(%rdi), %r14 + movq 40(%rdi), %r15 + movq 48(%rdi), %rsp + movq 56(%rdi), %rdx + movl %esi, %eax + testl %eax, %eax + jne 1f + movl $1, %eax +1: + jmp *%rdx +.size longjmp, .-longjmp + + +/* Allocation-scope push. Written in assembly so it never clobbers the + * System V argument registers (rdi/rsi/rdx/rcx/r8/r9) before the function + * body has a chance to read its parameters. */ +.global runtime_scope_push +.type runtime_scope_push,@function +runtime_scope_push: + movq fr_scope_depth(%rip), %rax + cmpq $1024, %rax + jae 2f + leaq fr_scope_heads(%rip), %r11 + movq $0, (%r11,%rax,8) + incq %rax + movq %rax, fr_scope_depth(%rip) + ret +2: + incq fr_scope_overflow(%rip) + ret +.size runtime_scope_push, .-runtime_scope_push diff --git a/runtime/vm.c b/runtime/vm.c index 2c2d592..767c745 100644 --- a/runtime/vm.c +++ b/runtime/vm.c @@ -262,6 +262,7 @@ static char *fr_strndup(const char *s, size_t n) { typedef struct Value Value; typedef struct List List; typedef struct Set Set; +typedef struct Dict Dict; typedef struct Struct Struct; typedef struct VM VM; // Forward declare VM @@ -321,7 +322,9 @@ typedef enum VAL_VOID, VAL_LIST, // List/Array type VAL_SET, // Set type (unordered collection of unique elements) + VAL_DICT, // Dictionary type VAL_STRUCT, // Struct/Object type + VAL_PTR, // Pointer to a Value cell (address-of) VAL_PYOBJECT // Python object type (PyObject*) } ValueType; @@ -331,6 +334,7 @@ typedef struct List Value *items; // Dynamic array of values int length; // Current number of items int capacity; // Allocated capacity + int refcount; // Reference count (mutable objects are shared) } List; // Struct structure @@ -339,6 +343,7 @@ typedef struct Struct int struct_id; // ID identifying which struct type this is Value *fields; // Dynamic array of field values int field_count; // Number of fields + int refcount; // Reference count (structs are reference semantics) } Struct; // Runtime value @@ -354,7 +359,9 @@ typedef struct Value bool boolean; List *list; // List pointer Set *set; // Set pointer + Dict *dict_val; // Dictionary pointer Struct *struct_val; // Struct pointer + Value *ptr; // Pointer to another Value cell PyObject *pyobj; // Python object pointer } as; } Value; @@ -374,8 +381,22 @@ typedef struct Set int length; // Number of elements in set int capacity; // Allocated capacity int tombstones; // Count of deleted entries + int refcount; // Reference count } Set; +typedef struct DictEntry { + Value key; + Value value; + bool occupied; +} DictEntry; + +struct Dict { + DictEntry *entries; + int length; + int capacity; + int refcount; +}; + // Variable storage typedef struct { @@ -396,6 +417,8 @@ typedef enum OP_CONST_BOOL_MULTI, // Push multiple bool constants OP_LOAD, OP_STORE, + OP_LOAD_GLOBAL, + OP_STORE_GLOBAL, OP_STORE_CONST_I64, // Store int constants directly to slots without using stack (STORE_CONST_I64 slot1 val1 slot2 val2 ...) OP_STORE_CONST_F64, // Store float constants directly to slots without using stack (STORE_CONST_F64 slot1 val1 slot2 val2 ...) OP_STORE_CONST_BOOL, // Store bool constants directly to slots without using stack (STORE_CONST_BOOL slot1 val1 slot2 val2 ...) @@ -476,6 +499,7 @@ typedef enum OP_LOAD_MULTI, // Load multiple variables at once (LOAD_MULTI n v1 v2 ... vn) OP_FUSED_LOAD_STORE, // Interleaved load/store (FUSED_LOAD_STORE n src1 dst1 src2 dst2 ...) OP_FUSED_STORE_LOAD, // Interleaved store/load (FUSED_STORE_LOAD n dst1 src1 dst2 src2 ...) + OP_FUSED_GET_STORE_LOAD, // Triplets: STRUCT_GET field, STORE dst, LOAD src // Fused load + arithmetic operations (reduce dispatch overhead in hot loops) OP_LOAD2_ADD_I64, // LOAD x y, ADD_I64 -> single instruction OP_LOAD2_SUB_I64, // LOAD x y, SUB_I64 -> single instruction @@ -509,6 +533,7 @@ typedef enum OP_LIST_NEW_F64, // Create list with float64 values (count val1 val2 ... -> list) OP_LIST_NEW_STR, // Create list with string values (count val1 val2 ... -> list) OP_LIST_NEW_BOOL, // Create list with boolean values (count val1 val2 ... -> list) + OP_LIST_NEW_CAP, // Create fixed-size list: LIST_NEW_CAP // Set operations (hash table for O(1) operations) OP_SET_NEW, // Create new empty set (-> set) @@ -517,6 +542,13 @@ typedef enum OP_SET_CONTAINS, // Check if set contains value (set, value -> bool) OP_SET_LEN, // Get set length (set -> int) + // Dictionary operations + OP_DICT_NEW, // Create empty dict (-> dict) + OP_DICT_SET, // Set dict[key] = value (dict, key, value -> dict) + OP_DICT_GET, // Get dict[key] (dict, key -> value) + OP_DICT_CONTAINS, // Check key membership (dict, key -> bool) + OP_DICT_LEN, // Get dict length (dict -> int) + // Container membership check OP_CONTAINS, // Generic membership check (container, value -> bool) @@ -558,6 +590,12 @@ typedef enum // Struct operations OP_STRUCT_NEW, // Create new struct instance (struct_id -> struct) OP_STRUCT_GET, // Get field from struct (struct, field_idx -> value) + OP_ADDR_LOCAL, // Push pointer to a local variable slot + OP_ADDR_GLOBAL, // Push pointer to a global variable + OP_ADDR_FIELD, // Pop struct, push pointer to field + OP_ADDR_ELEM, // Pop list/index, push pointer to element + OP_DEREF, // Pop pointer, push pointed-to value + OP_DEREF_SET, // Pop value, pointer; store through pointer OP_STRUCT_SET, // Set field in struct (struct, field_idx, value -> struct) // File I/O operations @@ -589,6 +627,7 @@ typedef enum OP_SOCKET_LISTEN, // Listen on socket (sock_id, backlog -> void) OP_SOCKET_ACCEPT, // Accept connection (sock_id -> client_sock_id) OP_SOCKET_SEND, // Send data (sock_id, data -> bytes_sent) + OP_SOCKET_SENDALL, // Send all data (sock_id, data -> void) OP_SOCKET_RECV, // Receive data (sock_id, size -> string) OP_SOCKET_CLOSE, // Close socket (sock_id -> void) OP_SOCKET_SETSOCKOPT, // Set socket option (sock_id, level, option, value -> void) @@ -722,6 +761,9 @@ typedef struct VM bool running; int exit_code; // Exit code for the program (0 = success, 1 = error) + Value globals[MAX_VARS]; // Global variable storage + char *global_names[MAX_VARS]; // Global variable names + int global_count; // Number of globals StructDef structs[MAX_STRUCTS]; // Struct definitions int struct_count; // Number of registered structs int prog_argc; // Program argument count @@ -899,7 +941,7 @@ void string_intern_destroy(StringIntern *intern) { void value_free(Value v); Value value_copy(Value v); void list_free(List *list); -void vm_runtime_error(VM *vm, const char *message, int char_pos); +bool vm_runtime_error(VM *vm, const char *message, int char_pos); // Safe parsing helpers (use strtol family instead of unsafe ato* functions) static inline int64_t safe_atoll(const char *str) @@ -984,6 +1026,14 @@ Value value_make_void() return v; } +static double value_as_double(Value v) +{ + if (v.type == VAL_F64) return v.as.f64; + if (v.type == VAL_INT) return (double)v.as.int64; + if (v.type == VAL_BIGINT && v.as.bigint) return mpz_get_d(*v.as.bigint); + return 0.0; +} + // List helper functions List *list_new() { List *list = malloc(sizeof(List)); @@ -994,6 +1044,7 @@ List *list_new() { } list->capacity = 8; // Start with capacity of 8 list->length = 0; + list->refcount = 1; list->items = malloc(sizeof(Value) * list->capacity); if (!list->items) { @@ -1070,7 +1121,9 @@ Value list_get(List *list, int index) { } if (g_current_vm) { - vm_runtime_error(g_current_vm, error_msg, char_pos); + if (vm_runtime_error(g_current_vm, error_msg, char_pos)) { + return value_make_int_si(0); + } } else { fprintf(stderr, "Error: %s\n", error_msg); } @@ -1131,7 +1184,9 @@ void list_set(List *list, int index, Value value) { } if (g_current_vm) { - vm_runtime_error(g_current_vm, error_msg, char_pos); + if (vm_runtime_error(g_current_vm, error_msg, char_pos)) { + return; + } } else { fprintf(stderr, "Error: %s\n", error_msg); } @@ -1211,6 +1266,8 @@ static bool value_equals(Value a, Value b) { return a.as.boolean == b.as.boolean; case VAL_VOID: return true; + case VAL_DICT: + return a.as.dict_val == b.as.dict_val; default: // For complex types, compare pointers return a.as.list == b.as.list; @@ -1226,6 +1283,7 @@ Set *set_new() { set->capacity = 16; // Start with capacity of 16 set->length = 0; set->tombstones = 0; + set->refcount = 1; set->entries = calloc(set->capacity, sizeof(SetEntry)); if (!set->entries) { fprintf(stderr, "Error: Failed to allocate set entries\n"); @@ -1370,6 +1428,86 @@ Value value_wrap_set(Set *set) { return v; } +Dict *dict_new() { + Dict *dict = malloc(sizeof(Dict)); + if (!dict) { + fprintf(stderr, "Error: Failed to allocate dict\n"); + exit(1); + } + dict->capacity = 8; + dict->length = 0; + dict->refcount = 1; + dict->entries = calloc(dict->capacity, sizeof(DictEntry)); + if (!dict->entries) { + fprintf(stderr, "Error: Failed to allocate dict entries\n"); + exit(1); + } + return dict; +} + +static int dict_find(Dict *dict, Value key) { + for (int i = 0; i < dict->length; i++) { + if (dict->entries[i].occupied && value_equals(dict->entries[i].key, key)) { + return i; + } + } + return -1; +} + +void dict_set(Dict *dict, Value key, Value value) { + if (!dict) return; + int idx = dict_find(dict, key); + if (idx >= 0) { + value_free(dict->entries[idx].value); + dict->entries[idx].value = value_copy(value); + return; + } + if (dict->length >= dict->capacity) { + int cap = dict->capacity * 2; + DictEntry *entries = realloc(dict->entries, sizeof(DictEntry) * cap); + if (!entries) return; + memset(entries + dict->capacity, 0, sizeof(DictEntry) * (cap - dict->capacity)); + dict->entries = entries; + dict->capacity = cap; + } + dict->entries[dict->length].key = value_copy(key); + dict->entries[dict->length].value = value_copy(value); + dict->entries[dict->length].occupied = true; + dict->length++; +} + +Value dict_get(Dict *dict, Value key) { + if (!dict) return value_make_int_si(0); + int idx = dict_find(dict, key); + if (idx < 0) return value_make_int_si(0); + return value_copy(dict->entries[idx].value); +} + +bool dict_contains(Dict *dict, Value key) { + return dict && dict_find(dict, key) >= 0; +} + +int dict_len(Dict *dict) { return dict ? dict->length : 0; } + +void dict_free(Dict *dict) { + if (!dict) return; + for (int i = 0; i < dict->length; i++) { + if (dict->entries[i].occupied) { + value_free(dict->entries[i].key); + value_free(dict->entries[i].value); + } + } + free(dict->entries); + free(dict); +} + +Value value_make_dict() { + Value v; + v.type = VAL_DICT; + v.as.dict_val = dict_new(); + return v; +} + void value_free(Value v) { if (v.type == VAL_BIGINT && v.as.bigint) { @@ -1382,21 +1520,27 @@ void value_free(Value v) { } else if (v.type == VAL_LIST && v.as.list) { - list_free(v.as.list); + if (--v.as.list->refcount <= 0) list_free(v.as.list); } else if (v.type == VAL_SET && v.as.set) { - set_free(v.as.set); + if (--v.as.set->refcount <= 0) set_free(v.as.set); + } + else if (v.type == VAL_DICT && v.as.dict_val) + { + if (--v.as.dict_val->refcount <= 0) dict_free(v.as.dict_val); } else if (v.type == VAL_STRUCT && v.as.struct_val) { - // Free all field values - for (int i = 0; i < v.as.struct_val->field_count; i++) + if (--v.as.struct_val->refcount <= 0) { - value_free(v.as.struct_val->fields[i]); + for (int i = 0; i < v.as.struct_val->field_count; i++) + { + value_free(v.as.struct_val->fields[i]); + } + free(v.as.struct_val->fields); + free(v.as.struct_val); } - free(v.as.struct_val->fields); - free(v.as.struct_val); } else if (v.type == VAL_PYOBJECT && v.as.pyobj) { @@ -1425,45 +1569,27 @@ Value value_copy(Value v) } else if (v.type == VAL_LIST) { - // Deep copy list - Value result; - result.type = VAL_LIST; - result.as.list = list_new(); - for (int i = 0; i < v.as.list->length; i++) - { - list_append(result.as.list, v.as.list->items[i]); - } - return result; + v.as.list->refcount++; + return v; } else if (v.type == VAL_SET) { - // Deep copy set - Value result; - result.type = VAL_SET; - result.as.set = set_new(); - for (int i = 0; i < v.as.set->capacity; i++) - { - if (v.as.set->entries[i].occupied) - { - set_add(result.as.set, v.as.set->entries[i].key); - } - } - return result; + v.as.set->refcount++; + return v; + } + else if (v.type == VAL_DICT) + { + v.as.dict_val->refcount++; + return v; } else if (v.type == VAL_STRUCT) { - // Deep copy struct - Value result; - result.type = VAL_STRUCT; - result.as.struct_val = malloc(sizeof(Struct)); - result.as.struct_val->struct_id = v.as.struct_val->struct_id; - result.as.struct_val->field_count = v.as.struct_val->field_count; - result.as.struct_val->fields = malloc(sizeof(Value) * result.as.struct_val->field_count); - for (int i = 0; i < result.as.struct_val->field_count; i++) - { - result.as.struct_val->fields[i] = value_copy(v.as.struct_val->fields[i]); - } - return result; + v.as.struct_val->refcount++; + return v; + } + else if (v.type == VAL_PTR) + { + return v; } else if (v.type == VAL_PYOBJECT) { @@ -1528,6 +1654,7 @@ void vm_init(VM *vm) vm->running = false; vm->exit_code = 0; vm->struct_count = 0; + vm->global_count = 0; vm->prog_argc = 0; vm->prog_argv = NULL; vm->loaded_libs = NULL; @@ -1577,6 +1704,12 @@ void vm_free(VM *vm) if (vm->exception_handlers[i].exc_type) free(vm->exception_handlers[i].exc_type); } + // Free global values and names + for (int i = 0; i < vm->global_count; i++) + { + value_free(vm->globals[i]); + free(vm->global_names[i]); + } // Free function names for (int i = 0; i < vm->func_count; i++) { @@ -1673,7 +1806,7 @@ void vm_free(VM *vm) } // Helper function to format runtime errors with debug information -void vm_runtime_error(VM *vm, const char *message, int char_pos) { +bool vm_runtime_error(VM *vm, const char *message, int char_pos) { // Check if there's an active exception handler if (vm->exception_handler_count > 0) { // Find the most recent handler @@ -1732,7 +1865,7 @@ void vm_runtime_error(VM *vm, const char *message, int char_pos) { handler->exc_type = NULL; vm->exception_handler_count--; - return; // Continue execution at handler + return true; // Continue execution at handler } // Handler doesn't match - pop it and continue searching or error @@ -1743,8 +1876,7 @@ void vm_runtime_error(VM *vm, const char *message, int char_pos) { // Recursively check for other handlers // Use display_message to avoid showing the [Type] marker if (vm->exception_handler_count > 0) { - vm_runtime_error(vm, display_message, char_pos); - return; + return vm_runtime_error(vm, display_message, char_pos); } // No more handlers - will fall through to error printing @@ -1793,6 +1925,7 @@ void vm_runtime_error(VM *vm, const char *message, int char_pos) { } exit(1); + return false; } // Stack operations - inline for performance with always_inline hint @@ -1882,6 +2015,12 @@ void value_print(Value val) // Print struct in a simple format (can be improved) printf(""); break; + case VAL_PTR: + printf(""); + break; + case VAL_DICT: + printf(""); + break; case VAL_PYOBJECT: #if defined(FR_NO_PYTHON) printf(""); @@ -2000,6 +2139,12 @@ Value value_to_string(Value val) case VAL_STRUCT: result.as.str = strdup(""); break; + case VAL_PTR: + result.as.str = strdup(""); + break; + case VAL_DICT: + result.as.str = strdup(""); + break; case VAL_PYOBJECT: #if defined(FR_NO_PYTHON) result.as.str = strdup(""); @@ -2772,6 +2917,28 @@ bool vm_load_bytecode(VM *vm, const char *filename) { // Skip this directive as it's for static analysis/debugging continue; } + else if (strcmp(token, ".global") == 0) + { + char *name = strtok(NULL, " "); + char *gtype = strtok(NULL, " "); + if (!name) continue; + if (vm->global_count >= MAX_VARS) { + fprintf(stderr, "Too many global variables\n"); + return false; + } + int idx = vm->global_count++; + vm->global_names[idx] = strdup(name); + if (gtype && (strcmp(gtype, "f64") == 0 || strcmp(gtype, "float") == 0)) { + vm->globals[idx] = value_make_f64(0.0); + } else if (gtype && (strcmp(gtype, "str") == 0 || strcmp(gtype, "string") == 0)) { + vm->globals[idx] = value_make_str(""); + } else if (gtype && (strcmp(gtype, "bool") == 0)) { + vm->globals[idx] = value_make_bool(false); + } else { + vm->globals[idx] = value_make_int_si(0); + } + continue; + } else if (strcmp(token, "LABEL") == 0) { char *name = strtok(NULL, " "); @@ -2866,7 +3033,8 @@ bool vm_load_bytecode(VM *vm, const char *filename) { inst.operand.ptr = multi; } } - else if (strcmp(token, "CONST_STR") == 0) + else if (strcmp(token, "CONST_STR") == 0 || + strcmp(token, "CONST_BYTES") == 0) { // Support CONST_STR with multiple arguments char *rest_of_line = strtok(NULL, "\n"); @@ -3239,6 +3407,12 @@ bool vm_load_bytecode(VM *vm, const char *filename) { inst.op = OP_BUILTIN_LEN; else if (strcmp(token, "BUILTIN_SQRT") == 0) inst.op = OP_BUILTIN_SQRT; + else if (strcmp(token, "SIN") == 0 || strcmp(token, "BUILTIN_SIN") == 0) + inst.op = OP_SIN; + else if (strcmp(token, "COS") == 0 || strcmp(token, "BUILTIN_COS") == 0) + inst.op = OP_COS; + else if (strcmp(token, "TAN") == 0 || strcmp(token, "BUILTIN_TAN") == 0) + inst.op = OP_TAN; else if (strcmp(token, "BUILTIN_ROUND") == 0) inst.op = OP_BUILTIN_ROUND; else if (strcmp(token, "BUILTIN_FLOOR") == 0) @@ -3269,8 +3443,35 @@ bool vm_load_bytecode(VM *vm, const char *filename) { inst.op = OP_SET_CONTAINS; else if (strcmp(token, "SET_LEN") == 0) inst.op = OP_SET_LEN; + else if (strcmp(token, "DICT_NEW") == 0) + inst.op = OP_DICT_NEW; + else if (strcmp(token, "DICT_SET") == 0) + inst.op = OP_DICT_SET; + else if (strcmp(token, "DICT_GET") == 0) + inst.op = OP_DICT_GET; + else if (strcmp(token, "DICT_CONTAINS") == 0) + inst.op = OP_DICT_CONTAINS; + else if (strcmp(token, "DICT_LEN") == 0) + inst.op = OP_DICT_LEN; else if (strcmp(token, "CONTAINS") == 0) inst.op = OP_CONTAINS; + else if (strcmp(token, "LIST_NEW_CAP") == 0) + { + char *rest_of_line = strtok(NULL, "\n"); + if (rest_of_line == NULL) + { + fprintf(stderr, "Error: LIST_NEW_CAP requires capacity and type\n"); + exit(1); + } + MultiInt *multi = malloc(sizeof(MultiInt) + 2 * sizeof(int)); + multi->count = 2; + char *cap_str = strtok(rest_of_line, " "); + char *type_str = strtok(NULL, " "); + multi->values[0] = cap_str ? safe_atoi(cap_str) : 0; + multi->values[1] = type_str ? safe_atoi(type_str) : -1; + inst.op = OP_LIST_NEW_CAP; + inst.operand.ptr = multi; + } else if (strcmp(token, "LIST_NEW_I64") == 0) { // LIST_NEW_I64 count val1 val2 val3 ... @@ -3458,6 +3659,48 @@ bool vm_load_bytecode(VM *vm, const char *filename) { inst.operand.ptr = multi; } // Struct operations + else if (strcmp(token, "ADDR_LOCAL") == 0) + { + char *idx_str = strtok(NULL, " \n"); + inst.op = OP_ADDR_LOCAL; + inst.operand.index = idx_str ? safe_atoi(idx_str) : 0; + } + else if (strcmp(token, "ADDR_GLOBAL") == 0) + { + char *name = strtok(NULL, " \n"); + inst.op = OP_ADDR_GLOBAL; + inst.operand.ptr = name ? strdup(name) : strdup(""); + } + else if (strcmp(token, "ADDR_FIELD") == 0) + { + char *idx_str = strtok(NULL, " \n"); + inst.op = OP_ADDR_FIELD; + inst.operand.index = idx_str ? safe_atoi(idx_str) : 0; + } + else if (strcmp(token, "ADDR_ELEM") == 0) + { + inst.op = OP_ADDR_ELEM; + } + else if (strcmp(token, "DEREF") == 0) + { + inst.op = OP_DEREF; + } + else if (strcmp(token, "DEREF_SET") == 0) + { + inst.op = OP_DEREF_SET; + } + else if (strcmp(token, "LOAD_GLOBAL") == 0) + { + char *name = strtok(NULL, " \n"); + inst.op = OP_LOAD_GLOBAL; + inst.operand.ptr = name ? strdup(name) : strdup(""); + } + else if (strcmp(token, "STORE_GLOBAL") == 0) + { + char *name = strtok(NULL, " \n"); + inst.op = OP_STORE_GLOBAL; + inst.operand.ptr = name ? strdup(name) : strdup(""); + } else if (strcmp(token, "STRUCT_NEW") == 0) { char *struct_id_str = strtok(NULL, " "); @@ -3561,6 +3804,8 @@ bool vm_load_bytecode(VM *vm, const char *filename) { inst.op = OP_SOCKET_ACCEPT; else if (strcmp(token, "SOCKET_SEND") == 0) inst.op = OP_SOCKET_SEND; + else if (strcmp(token, "SOCKET_SENDALL") == 0) + inst.op = OP_SOCKET_SENDALL; else if (strcmp(token, "SOCKET_RECV") == 0) inst.op = OP_SOCKET_RECV; else if (strcmp(token, "SOCKET_CLOSE") == 0) @@ -3974,6 +4219,29 @@ bool vm_load_bytecode(VM *vm, const char *filename) { } inst.operand.ptr = multi; } + // Fused struct field get + store + load triplets + else if (strcmp(token, "FUSED_GET_STORE_LOAD") == 0) + { + inst.op = OP_FUSED_GET_STORE_LOAD; + char *rest_of_line = strtok(NULL, "\n"); + if (rest_of_line == NULL) + { + fprintf(stderr, "Error: FUSED_GET_STORE_LOAD requires arguments\n"); + exit(1); + } + int temp_args[768]; + int arg_count = 0; + char *arg_str = strtok(rest_of_line, " "); + while (arg_str != NULL && arg_count < 768) + { + temp_args[arg_count++] = safe_atoi(arg_str); + arg_str = strtok(NULL, " "); + } + MultiInt *multi = malloc(sizeof(MultiInt) + arg_count * sizeof(int)); + multi->count = arg_count; + for (int j = 0; j < arg_count; j++) multi->values[j] = temp_args[j]; + inst.operand.ptr = multi; + } // Fused LOAD2 + arithmetic instructions else if (strcmp(token, "LOAD2_ADD_I64") == 0) { @@ -4207,6 +4475,7 @@ __attribute__((hot)) void vm_run(VM *vm) { { // Create a list of argument strings List *argv_list = malloc(sizeof(List)); + argv_list->refcount = 1; argv_list->length = vm->prog_argc; argv_list->capacity = (vm->prog_argc > 0) ? vm->prog_argc : 1; // At least capacity of 1 argv_list->items = malloc(sizeof(Value) * argv_list->capacity); @@ -4242,6 +4511,8 @@ __attribute__((hot)) void vm_run(VM *vm) { [OP_CONST_BOOL_MULTI] = &&L_CONST_BOOL_MULTI, [OP_LOAD] = &&L_LOAD, [OP_STORE] = &&L_STORE, + [OP_LOAD_GLOBAL] = &&L_LOAD_GLOBAL, + [OP_STORE_GLOBAL] = &&L_STORE_GLOBAL, [OP_STORE_CONST_I64] = &&L_STORE_CONST_I64, [OP_STORE_CONST_F64] = &&L_STORE_CONST_F64, [OP_STORE_CONST_BOOL] = &&L_STORE_CONST_BOOL, @@ -4277,6 +4548,9 @@ __attribute__((hot)) void vm_run(VM *vm) { [OP_BUILTIN_STR] = &&L_BUILTIN_STR, [OP_BUILTIN_LEN] = &&L_BUILTIN_LEN, [OP_BUILTIN_SQRT] = &&L_BUILTIN_SQRT, + [OP_SIN] = &&L_SIN, + [OP_COS] = &&L_COS, + [OP_TAN] = &&L_TAN, [OP_BUILTIN_ROUND] = &&L_BUILTIN_ROUND, [OP_BUILTIN_FLOOR] = &&L_BUILTIN_FLOOR, [OP_BUILTIN_CEIL] = &&L_BUILTIN_CEIL, @@ -4317,6 +4591,7 @@ __attribute__((hot)) void vm_run(VM *vm) { [OP_LOAD_MULTI] = &&L_LOAD_MULTI, [OP_FUSED_LOAD_STORE] = &&L_FUSED_LOAD_STORE, [OP_FUSED_STORE_LOAD] = &&L_FUSED_STORE_LOAD, + [OP_FUSED_GET_STORE_LOAD] = &&L_FUSED_GET_STORE_LOAD, [OP_LOAD2_ADD_I64] = &&L_LOAD2_ADD_I64, [OP_LOAD2_SUB_I64] = &&L_LOAD2_SUB_I64, [OP_LOAD2_MUL_I64] = &&L_LOAD2_MUL_I64, @@ -4343,14 +4618,26 @@ __attribute__((hot)) void vm_run(VM *vm) { [OP_LIST_NEW_F64] = &&L_LIST_NEW_F64, [OP_LIST_NEW_STR] = &&L_LIST_NEW_STR, [OP_LIST_NEW_BOOL] = &&L_LIST_NEW_BOOL, + [OP_LIST_NEW_CAP] = &&L_LIST_NEW_CAP, [OP_SET_NEW] = &&L_SET_NEW, [OP_SET_ADD] = &&L_SET_ADD, [OP_SET_REMOVE] = &&L_SET_REMOVE, [OP_SET_CONTAINS] = &&L_SET_CONTAINS, [OP_SET_LEN] = &&L_SET_LEN, + [OP_DICT_NEW] = &&L_DICT_NEW, + [OP_DICT_SET] = &&L_DICT_SET, + [OP_DICT_GET] = &&L_DICT_GET, + [OP_DICT_CONTAINS] = &&L_DICT_CONTAINS, + [OP_DICT_LEN] = &&L_DICT_LEN, [OP_CONTAINS] = &&L_CONTAINS, [OP_STRUCT_NEW] = &&L_STRUCT_NEW, [OP_STRUCT_GET] = &&L_STRUCT_GET, + [OP_ADDR_LOCAL] = &&L_ADDR_LOCAL, + [OP_ADDR_GLOBAL] = &&L_ADDR_GLOBAL, + [OP_ADDR_FIELD] = &&L_ADDR_FIELD, + [OP_ADDR_ELEM] = &&L_ADDR_ELEM, + [OP_DEREF] = &&L_DEREF, + [OP_DEREF_SET] = &&L_DEREF_SET, [OP_STRUCT_SET] = &&L_STRUCT_SET, [OP_TO_INT] = &&L_TO_INT, [OP_TO_FLOAT] = &&L_TO_FLOAT, @@ -4395,6 +4682,7 @@ __attribute__((hot)) void vm_run(VM *vm) { [OP_SOCKET_LISTEN] = &&L_SOCKET_LISTEN, [OP_SOCKET_ACCEPT] = &&L_SOCKET_ACCEPT, [OP_SOCKET_SEND] = &&L_SOCKET_SEND, + [OP_SOCKET_SENDALL] = &&L_SOCKET_SENDALL, [OP_SOCKET_RECV] = &&L_SOCKET_RECV, [OP_SOCKET_CLOSE] = &&L_SOCKET_CLOSE, [OP_SOCKET_SETSOCKOPT] = &&L_SOCKET_SETSOCKOPT, @@ -4491,6 +4779,41 @@ __attribute__((hot)) void vm_run(VM *vm) { } DISPATCH(); } +L_LOAD_GLOBAL: // OP_LOAD_GLOBAL +{ + Instruction inst = vm->code[vm->pc - 1]; + char *name = (char *)inst.operand.ptr; + int idx = -1; + for (int i = 0; i < vm->global_count; i++) { + if (strcmp(vm->global_names[i], name) == 0) { idx = i; break; } + } + if (idx < 0) { + fprintf(stderr, "Error: Unknown global '%s'\n", name ? name : ""); + exit(1); + } + vm_push(vm, value_copy(vm->globals[idx])); + DISPATCH(); +} + +L_STORE_GLOBAL: // OP_STORE_GLOBAL +{ + Instruction inst = vm->code[vm->pc - 1]; + char *name = (char *)inst.operand.ptr; + Value val = vm_pop(vm); + int idx = -1; + for (int i = 0; i < vm->global_count; i++) { + if (strcmp(vm->global_names[i], name) == 0) { idx = i; break; } + } + if (idx < 0) { + fprintf(stderr, "Error: Unknown global '%s'\n", name ? name : ""); + value_free(val); + exit(1); + } + value_free(vm->globals[idx]); + vm->globals[idx] = val; + DISPATCH(); +} + L_LOAD: // OP_LOAD { Instruction inst = vm->code[vm->pc - 1]; @@ -4919,9 +5242,11 @@ __attribute__((hot)) void vm_run(VM *vm) { CallFrame *frame = &vm->call_stack[vm->call_stack_top++]; frame->func = func; frame->return_pc = vm->pc; - frame->vars.var_count = func->local_count; - // Initialize locals to 0 - for (int i = 0; i < func->local_count; i++) + frame->vars.var_count = MAX_VARS; + // Initialize every slot. Compiler-generated field-cache temporaries can + // use indices beyond the declared local count; leaving those slots with + // recycled pointers makes STORE free an arbitrary old value. + for (int i = 0; i < MAX_VARS; i++) { frame->vars.vars[i] = value_make_int_si(0); } @@ -5060,6 +5385,27 @@ __attribute__((hot)) void vm_run(VM *vm) { DISPATCH(); } } +L_SIN: // OP_SIN +{ + Value v = vm_pop(vm); + vm_push(vm, value_make_f64(sin(value_as_double(v)))); + value_free(v); + DISPATCH(); +} +L_COS: // OP_COS +{ + Value v = vm_pop(vm); + vm_push(vm, value_make_f64(cos(value_as_double(v)))); + value_free(v); + DISPATCH(); +} +L_TAN: // OP_TAN +{ + Value v = vm_pop(vm); + vm_push(vm, value_make_f64(tan(value_as_double(v)))); + value_free(v); + DISPATCH(); +} L_BUILTIN_ROUND: // OP_BUILTIN_ROUND { { @@ -6394,6 +6740,62 @@ __attribute__((hot)) void vm_run(VM *vm) { } // Set operations +L_DICT_NEW: // OP_DICT_NEW +{ + vm_push(vm, value_make_dict()); + DISPATCH(); +} + +L_DICT_SET: // OP_DICT_SET +{ + Value value = vm_pop(vm); + Value key = vm_pop(vm); + Value d = vm_pop(vm); + if (d.type != VAL_DICT) { + fprintf(stderr, "Runtime error: DICT_SET on non-dict\n"); + value_free(value); value_free(key); value_free(d); + exit(1); + } + dict_set(d.as.dict_val, key, value); + value_free(value); value_free(key); + vm_push(vm, d); + DISPATCH(); +} + +L_DICT_GET: // OP_DICT_GET +{ + Value key = vm_pop(vm); + Value d = vm_pop(vm); + if (d.type != VAL_DICT) { + fprintf(stderr, "Runtime error: DICT_GET on non-dict\n"); + value_free(key); value_free(d); + exit(1); + } + Value result = dict_get(d.as.dict_val, key); + value_free(key); value_free(d); + vm_push(vm, result); + DISPATCH(); +} + +L_DICT_CONTAINS: // OP_DICT_CONTAINS +{ + Value key = vm_pop(vm); + Value d = vm_pop(vm); + bool found = d.type == VAL_DICT && dict_contains(d.as.dict_val, key); + value_free(key); value_free(d); + vm_push(vm, value_make_bool(found)); + DISPATCH(); +} + +L_DICT_LEN: // OP_DICT_LEN +{ + Value d = vm_pop(vm); + int n = d.type == VAL_DICT ? dict_len(d.as.dict_val) : 0; + value_free(d); + vm_push(vm, value_make_int_si(n)); + DISPATCH(); +} + L_SET_NEW: // OP_SET_NEW { vm_push(vm, value_make_set()); @@ -6575,6 +6977,142 @@ __attribute__((hot)) void vm_run(VM *vm) { DISPATCH(); } +L_LIST_NEW_CAP: // OP_LIST_NEW_CAP +{ + Instruction inst = vm->code[vm->pc - 1]; + MultiInt *multi = (MultiInt*)inst.operand.ptr; + int capacity = multi->count > 0 ? multi->values[0] : 0; + int elem_type = multi->count > 1 ? multi->values[1] : -1; + if (capacity < 0) capacity = 0; + + List *list = list_new(); + list->capacity = capacity > 0 ? capacity : 1; + list->items = realloc(list->items, sizeof(Value) * list->capacity); + list->length = capacity; + + for (int i = 0; i < capacity; i++) + { + switch (elem_type) + { + case 1: list->items[i] = value_make_str(""); break; + case 2: list->items[i] = value_make_f64(0.0); break; + case 3: list->items[i] = value_make_bool(false); break; + default: list->items[i] = value_make_int_si(0); break; + } + } + vm_push(vm, value_wrap_list(list)); + DISPATCH(); +} + +L_ADDR_LOCAL: // OP_ADDR_LOCAL +{ + Instruction inst = vm->code[vm->pc - 1]; + CallFrame *frame = &vm->call_stack[vm->call_stack_top - 1]; + int idx = inst.operand.index; + if (idx < 0 || idx >= MAX_VARS) + { + fprintf(stderr, "Error: Invalid local index for ADDR_LOCAL\n"); + exit(1); + } + Value ptr; + ptr.type = VAL_PTR; + ptr.as.ptr = &frame->vars.vars[idx]; + vm_push(vm, ptr); + DISPATCH(); +} + +L_ADDR_GLOBAL: // OP_ADDR_GLOBAL +{ + Instruction inst = vm->code[vm->pc - 1]; + char *name = (char *)inst.operand.ptr; + int idx = -1; + for (int i = 0; i < vm->global_count; i++) { + if (strcmp(vm->global_names[i], name) == 0) { idx = i; break; } + } + if (idx < 0) { + fprintf(stderr, "Error: Unknown global '%s'\n", name ? name : ""); + exit(1); + } + Value ptr; + ptr.type = VAL_PTR; + ptr.as.ptr = &vm->globals[idx]; + vm_push(vm, ptr); + DISPATCH(); +} + +L_ADDR_FIELD: // OP_ADDR_FIELD +{ + Instruction inst = vm->code[vm->pc - 1]; + Value sv = vm_pop(vm); + if (sv.type != VAL_STRUCT) + { + fprintf(stderr, "Error: ADDR_FIELD requires a struct\n"); + exit(1); + } + int idx = inst.operand.index; + if (idx < 0 || idx >= sv.as.struct_val->field_count) + { + fprintf(stderr, "Error: Invalid struct field index\n"); + exit(1); + } + Value ptr; + ptr.type = VAL_PTR; + ptr.as.ptr = &sv.as.struct_val->fields[idx]; + vm_push(vm, ptr); + DISPATCH(); +} + +L_ADDR_ELEM: // OP_ADDR_ELEM +{ + Value idx_val = vm_pop(vm); + Value list_val = vm_pop(vm); + if (list_val.type != VAL_LIST) + { + fprintf(stderr, "Error: ADDR_ELEM requires a list\n"); + exit(1); + } + int idx = (int)(idx_val.type == VAL_INT ? idx_val.as.int64 : (int64_t)idx_val.as.f64); + value_free(idx_val); + if (idx < 0) idx += list_val.as.list->length; + if (idx < 0 || idx >= list_val.as.list->length) + { + fprintf(stderr, "Error: list index out of range for ADDR_ELEM\n"); + exit(1); + } + Value ptr; + ptr.type = VAL_PTR; + ptr.as.ptr = &list_val.as.list->items[idx]; + vm_push(vm, ptr); + DISPATCH(); +} + +L_DEREF: // OP_DEREF +{ + Value ptr = vm_pop(vm); + if (ptr.type != VAL_PTR || ptr.as.ptr == NULL) + { + fprintf(stderr, "Error: Cannot dereference a non-pointer value\n"); + exit(1); + } + vm_push(vm, value_copy(*ptr.as.ptr)); + DISPATCH(); +} + +L_DEREF_SET: // OP_DEREF_SET +{ + Value value = vm_pop(vm); + Value ptr = vm_pop(vm); + if (ptr.type != VAL_PTR || ptr.as.ptr == NULL) + { + fprintf(stderr, "Error: Cannot assign through a non-pointer value\n"); + value_free(value); + exit(1); + } + value_free(*ptr.as.ptr); + *ptr.as.ptr = value; + DISPATCH(); +} + L_STRUCT_NEW: // OP_STRUCT_NEW { Instruction inst = vm->code[vm->pc - 1]; @@ -6595,6 +7133,7 @@ __attribute__((hot)) void vm_run(VM *vm) { Struct *s = malloc(sizeof(Struct)); s->struct_id = struct_id; s->field_count = def->field_count; + s->refcount = 1; s->fields = malloc(sizeof(Value) * s->field_count); // Pop field values from stack in reverse order (last field pushed first) @@ -6629,7 +7168,9 @@ __attribute__((hot)) void vm_run(VM *vm) { if (field_idx < 0 || field_idx >= struct_val.as.struct_val->field_count) { - fprintf(stderr, "Error: Field index %d out of range\n", field_idx); + fprintf(stderr, "Error: Field index %d out of range (struct %d, fields %d)\n", + field_idx, struct_val.as.struct_val->struct_id, + struct_val.as.struct_val->field_count); value_free(struct_val); vm->running = false; vm->exit_code = 1; @@ -6662,7 +7203,9 @@ __attribute__((hot)) void vm_run(VM *vm) { if (field_idx < 0 || field_idx >= struct_val.as.struct_val->field_count) { - fprintf(stderr, "Error: Field index %d out of range\n", field_idx); + fprintf(stderr, "Error: Field index %d out of range (struct %d, fields %d)\n", + field_idx, struct_val.as.struct_val->struct_id, + struct_val.as.struct_val->field_count); value_free(struct_val); value_free(new_value); vm->running = false; @@ -7271,6 +7814,7 @@ __attribute__((hot)) void vm_run(VM *vm) { } List *list = malloc(sizeof(List)); + list->refcount = 1; list->capacity = 16; list->length = 0; list->items = malloc(sizeof(Value) * list->capacity); @@ -7776,7 +8320,7 @@ __attribute__((hot)) void vm_run(VM *vm) { int sock = (int)sock_val.as.int64; const char *data = data_val.as.str; - int sent = send(sock, data, strlen(data), 0); + int sent = send(sock, data, strlen(data), MSG_NOSIGNAL); value_free(sock_val); value_free(data_val); @@ -7784,6 +8328,32 @@ __attribute__((hot)) void vm_run(VM *vm) { DISPATCH(); } +L_SOCKET_SENDALL: // OP_SOCKET_SENDALL - Send all data +{ + Value data_val = vm_pop(vm); + Value sock_val = vm_pop(vm); + if (sock_val.type != VAL_INT || data_val.type != VAL_STR) + { + fprintf(stderr, "Runtime error: sendall() expects (int, string)\n"); + value_free(sock_val); + value_free(data_val); + exit(1); + } + int sock = (int)sock_val.as.int64; + const char *data = data_val.as.str; + size_t total = strlen(data); + size_t sent = 0; + while (sent < total) + { + int n = send(sock, data + sent, total - sent, MSG_NOSIGNAL); + if (n <= 0) break; + sent += (size_t)n; + } + value_free(sock_val); + value_free(data_val); + DISPATCH(); +} + L_SOCKET_RECV: // OP_SOCKET_RECV - Receive data (sock_id, size -> string) { Value size_val = vm_pop(vm); @@ -8411,6 +8981,38 @@ __attribute__((hot)) void vm_run(VM *vm) { DISPATCH(); } +L_FUSED_GET_STORE_LOAD: // OP_FUSED_GET_STORE_LOAD +{ + Instruction inst = vm->code[vm->pc - 1]; + MultiInt *multi = (MultiInt *)inst.operand.ptr; + CallFrame *frame = &vm->call_stack[vm->call_stack_top - 1]; + + for (int i = 0; i + 2 < multi->count; i += 3) + { + int field_idx = multi->values[i]; + int store_idx = multi->values[i + 1]; + int load_idx = multi->values[i + 2]; + + Value struct_val = vm_pop(vm); + if (struct_val.type != VAL_STRUCT || + field_idx < 0 || field_idx >= struct_val.as.struct_val->field_count) + { + fprintf(stderr, "Error: Invalid fused struct field access (field %d, type %d)\n", + field_idx, struct_val.type == VAL_STRUCT ? struct_val.as.struct_val->field_count : -1); + exit(1); + } + Value field_val = value_copy(struct_val.as.struct_val->fields[field_idx]); + value_free(struct_val); + + value_free(frame->vars.vars[store_idx]); + frame->vars.vars[store_idx] = value_copy(field_val); + value_free(field_val); + + vm_push(vm, value_copy(frame->vars.vars[load_idx])); + } + DISPATCH(); +} + // Fused LOAD2 + arithmetic operations (hot loop optimizations) L_LOAD2_ADD_I64: // OP_LOAD2_ADD_I64 { diff --git a/src/builtin_funcs.py b/src/builtin_funcs.py index 0988090..8e4b7a2 100644 --- a/src/builtin_funcs.py +++ b/src/builtin_funcs.py @@ -12,12 +12,55 @@ _python_modules = {} +def _round_c(num: float) -> int: + """C round(): ties round away from zero, not Python's banker's rounding.""" + return floor(num + 0.5) if num >= 0 else ceil(num - 0.5) + + +def _contains(container, value) -> bool: + """Membership test for strings, lists, sets and dictionaries.""" + if isinstance(container, bytes): + if isinstance(value, str): + value = value.encode("latin-1") + return value in container + if isinstance(container, str): + return str(value) in container + if isinstance(container, (list, set, dict, tuple)): + return value in container + return False + + # I/O helper functions +def _format_output(value) -> str: + """Format values the same way the bytecode/native runtimes do.""" + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, list): + return "[" + ", ".join(_format_output(v) for v in value) + "]" + if isinstance(value, set): + return "{" + ", ".join(_format_output(v) for v in value) + "}" + if isinstance(value, dict): + return "{" + ", ".join( + f"{_format_output(k)}: {_format_output(v)}" for k, v in value.items() + ) + "}" + if value is None: + return "None" + return str(value) + + def _print(text: str) -> None: - """Print text without newline""" + """Print a value without newline.""" import sys as _sys - _sys.stdout.write(str(text)) + _sys.stdout.write(_format_output(text)) + _sys.stdout.flush() + + +def _println(text: str) -> None: + """Print a value followed by a newline.""" + import sys as _sys + + _sys.stdout.write(_format_output(text) + "\n") _sys.stdout.flush() @@ -168,6 +211,7 @@ def _file_join(*paths: str): # Process management functions _fork_processes = {} # Maps synthetic pid -> subprocess.Popen _next_fork_pid = 1 +_in_fork_child = False def _fork() -> int: @@ -214,6 +258,8 @@ def _fork() -> int: try: pid = _os.fork() if pid == 0: + global _in_fork_child + _in_fork_child = True # Child process: set up to receive SIGTERM when parent dies # This is Linux-specific try: @@ -261,6 +307,12 @@ def _exit(code: int = 0) -> None: """Exit the program with the given exit code.""" import sys as _sys + if _in_fork_child: + # In a real fork the child must terminate without unwinding the + # parent's interpreter state or running its atexit handlers. + _sys.stdout.flush() + _sys.stderr.flush() + _os._exit(int(code)) _sys.exit(code) @@ -349,6 +401,15 @@ def _socket_send(sock_id: int, data: bytes): return sock.send(data) +def _socket_sendall(sock_id: int, data: bytes): + """Send all bytes, retrying partial writes.""" + if sock_id not in _socket_map: + raise RuntimeError(f"Invalid socket ID: {sock_id}") + sock = _socket_map[sock_id] + sock.sendall(data) + return None + + def _socket_recv(sock_id: int, size=4096): """Receive data from socket""" if sock_id not in _socket_map: @@ -560,7 +621,7 @@ def _py_call_method(obj, method_name: str, *args): "println": { "type": "builtin", "args": {"text": "string"}, - "func": print, + "func": _println, "return_type": "none", "can_eval": False, }, @@ -595,7 +656,7 @@ def _py_call_method(obj, method_name: str, *args): "round": { "type": "builtin", "args": {"num": "float"}, - "func": round, + "func": _round_c, "return_type": "int", "can_eval": True, }, @@ -696,6 +757,13 @@ def _py_call_method(obj, method_name: str, *args): "can_eval": True, }, # String manipulation + "contains": { + "type": "builtin", + "args": {"container": "any", "value": "any"}, + "func": _contains, + "return_type": "bool", + "can_eval": True, + }, "upper": { "type": "builtin", "args": {"text": "string"}, @@ -1043,6 +1111,13 @@ def _py_call_method(obj, method_name: str, *args): "return_type": "int", "can_eval": False, }, + "sendall": { + "type": "builtin", + "args": {"sock_id": "int", "data": "bytes"}, + "func": _socket_sendall, + "return_type": "none", + "can_eval": False, + }, "recv": { "type": "builtin", "args": {"sock_id": "int", "size": "int"}, diff --git a/src/cli.py b/src/cli.py index d4c4d73..000a09a 100644 --- a/src/cli.py +++ b/src/cli.py @@ -538,7 +538,7 @@ def decode_cmd(args): def native_cmd(args): """Compile bytecode to x86_64 native binary""" if len(args) < 1: - print("Usage: fr native [-o output] [-a|--asm] [--ssa] [--ssa-ir] [-Os]") + print("Usage: fr native [-o output] [-a|--asm] [--ssa] [--ssa-ir] [-Os] [--target linux|fr-os]") print(" -a, --asm: Keep assembly file") print(" --ssa: Use SSA IR superoptimizer pipeline") print(" --ssa-ir: Dump SSA IR instead of generating assembly") @@ -549,6 +549,15 @@ def native_cmd(args): keep_asm = "-a" in args or "--asm" in args dump_ir = "--ssa-ir" in args optimize_size = "-Os" in args + target = "linux" + for i, arg in enumerate(args): + if arg == "--target" and i + 1 < len(args): + target = args[i + 1] + elif arg.startswith("--target="): + target = arg.split("=", 1)[1] + if target not in ("linux", "fr-os"): + print(f"Error: unsupported target '{target}' (expected linux or fr-os)") + sys.exit(1) # Determine output filename if "-o" in args: @@ -579,14 +588,16 @@ def native_cmd(args): opt_level = max(opt_level, 2) if dump_ir: - ir_text = compile_native_ssa(bytecode, opt_level=opt_level, dump_ir=True) + ir_text = compile_native_ssa( + bytecode, opt_level=opt_level, dump_ir=True, target=target + ) print(ir_text) return optimize = ("-O" in args and "-O0" not in args) or optimize_size if optimize: print(f"Optimizer (level {opt_level})") - asm = compile_native_ssa(bytecode, opt_level) + asm = compile_native_ssa(bytecode, opt_level, target=target) # Always write assembly to temp file for building with open(asm_file, "w") as f: @@ -609,18 +620,41 @@ def native_cmd(args): c_obj = c_file.replace(".c", ".o") print(f"Compiling C file: {c_file}") - cflags = [ - "-Os" if optimize_size else "-O3", - "-march=native", - "-mtune=native", - "-fno-strict-aliasing", - "-fwrapv", - "-ffunction-sections", - "-fdata-sections", - "-fno-asynchronous-unwind-tables", - "-fno-stack-protector", - "-fno-builtin", - ] + if target == "fr-os": + cflags = [ + "-Os" if optimize_size else "-O2", + "-march=x86-64", + "-mtune=generic", + "-mno-red-zone", + "-mno-avx", + "-mno-avx2", + "-mno-fma", + "-mno-f16c", + "-fno-pic", + "-fno-pie", + "-ffreestanding", + "-fno-strict-aliasing", + "-fwrapv", + "-ffunction-sections", + "-fdata-sections", + "-fno-asynchronous-unwind-tables", + "-fno-stack-protector", + "-fno-builtin", + "-DFR_TARGET_FR_OS", + ] + else: + cflags = [ + "-Os" if optimize_size else "-O3", + "-march=native", + "-mtune=native", + "-fno-strict-aliasing", + "-fwrapv", + "-ffunction-sections", + "-fdata-sections", + "-fno-asynchronous-unwind-tables", + "-fno-stack-protector", + "-fno-builtin", + ] cflags = [f for f in cflags if f] result = subprocess.run( [ @@ -667,18 +701,41 @@ def native_cmd(args): check=True, capture_output=True ) - cflags = [ - "-Os" if optimize_size else "-O3", - "-march=native", - "-mtune=native", - "-fno-strict-aliasing", - "-fwrapv", - "-ffunction-sections", - "-fdata-sections", - "-fno-asynchronous-unwind-tables", - "-fno-stack-protector", - "-fno-builtin", - ] + if target == "fr-os": + cflags = [ + "-Os" if optimize_size else "-O2", + "-march=x86-64", + "-mtune=generic", + "-mno-red-zone", + "-mno-avx", + "-mno-avx2", + "-mno-fma", + "-mno-f16c", + "-fno-pic", + "-fno-pie", + "-ffreestanding", + "-fno-strict-aliasing", + "-fwrapv", + "-ffunction-sections", + "-fdata-sections", + "-fno-asynchronous-unwind-tables", + "-fno-stack-protector", + "-fno-builtin", + "-DFR_TARGET_FR_OS", + ] + else: + cflags = [ + "-Os" if optimize_size else "-O3", + "-march=native", + "-mtune=native", + "-fno-strict-aliasing", + "-fwrapv", + "-ffunction-sections", + "-fdata-sections", + "-fno-asynchronous-unwind-tables", + "-fno-stack-protector", + "-fno-builtin", + ] cflags = [f for f in cflags if f] gcc_flags = [ @@ -697,6 +754,7 @@ def native_cmd(args): "-Wl,--gc-sections", "-Wl,--build-id=none", "-Wl,--strip-all", + *(["-no-pie", "-Wl,-z,noexecstack"] if target == "fr-os" else []), *link_libs, ] gcc_flags = [f for f in gcc_flags if f] diff --git a/src/compiler.py b/src/compiler.py index 82639da..4fb005a 100644 --- a/src/compiler.py +++ b/src/compiler.py @@ -23,6 +23,9 @@ "input": "INPUT", "len": "BUILTIN_LEN", "sqrt": "BUILTIN_SQRT", + "sin": "SIN", + "cos": "COS", + "tan": "TAN", "round": "BUILTIN_ROUND", "floor": "BUILTIN_FLOOR", "ceil": "BUILTIN_CEIL", @@ -38,6 +41,7 @@ "split": "STR_SPLIT", "join": "STR_JOIN", "replace": "STR_REPLACE", + "contains": "CONTAINS", "abs": "ABS", "pow": "POW", "min": "MIN", @@ -76,6 +80,7 @@ "listen": "SOCKET_LISTEN", "accept": "SOCKET_ACCEPT", "send": "SOCKET_SEND", + "sendall": "SOCKET_SENDALL", "recv": "SOCKET_RECV", "sclose": "SOCKET_CLOSE", "setsockopt": "SOCKET_SETSOCKOPT", @@ -245,7 +250,7 @@ def emit_load(self, var_name: str): """Emit a LOAD or LOAD_GLOBAL instruction based on variable scope""" var_id = self.get_var_id(var_name) if var_name in self.global_vars: - self.emit(f"LOAD_GLOBAL {var_id}") + self.emit(f"LOAD_GLOBAL {var_name}") else: self.emit(f"LOAD {var_id}") @@ -253,7 +258,7 @@ def emit_store(self, var_name: str): """Emit a STORE or STORE_GLOBAL instruction based on variable scope""" var_id = self.get_var_id(var_name) if var_name in self.global_vars: - self.emit(f"STORE_GLOBAL {var_id}") + self.emit(f"STORE_GLOBAL {var_name}") else: self.emit(f"STORE {var_id}") @@ -341,17 +346,38 @@ def _ensure_struct_field_temp( self.temp_local_types[temp_name] = field_type return temp_name + def _collect_assigned_vars(self, node: Any, out: set[str]) -> None: + if isinstance(node, dict): + if node.get("type") == "var" and node.get("name"): + out.add(node["name"]) + elif node.get("type") == "field_assign": + target = node.get("target") + if isinstance(target, str): + out.add(target) + elif isinstance(target, dict) and "id" in target: + out.add(target["id"]) + for value in node.values(): + self._collect_assigned_vars(value, out) + elif isinstance(node, list): + for item in node: + self._collect_assigned_vars(item, out) + def _build_struct_field_cache(self, stmt: dict) -> Dict[tuple[str, str], str]: """Build per-statement cache mapping for repeated struct field reads.""" counts: Dict[tuple[str, str], int] = {} writes: set[tuple[str, str]] = set() + assigned: set[str] = set() self._collect_struct_field_reads(stmt, counts) self._collect_struct_field_writes(stmt, writes) + self._collect_assigned_vars(stmt, assigned) cache: Dict[tuple[str, str], str] = {} for (var_name, field_name), count in counts.items(): if count <= 0 or (var_name, field_name) in writes: continue + if var_name in assigned: + # The cache prologue would read an uninitialized / stale value. + continue field_type = self._get_struct_field_type(var_name, field_name) if not field_type: continue @@ -442,6 +468,8 @@ def get_expr_struct_type(self, expr: Any) -> str: var_type = self.var_types.get(var_name, "") if isinstance(var_type, str) and var_type.startswith("struct:"): var_type = var_type.split(":", 1)[1] + if isinstance(var_type, str) and var_type.startswith("*"): + var_type = var_type.lstrip("*") # Check if this is a struct type return var_type if var_type in self.struct_defs else "" # If it's a member access (nested struct field) @@ -458,12 +486,28 @@ def get_expr_struct_type(self, expr: Any) -> str: if field.get("name") == field_name: # Get the field's type field_type = field.get("type", "") + while isinstance(field_type, str) and field_type.startswith("*"): + field_type = field_type[1:] # Check if this field type is a struct return field_type if field_type in self.struct_defs else "" return "" return "" + def expr_is_pointer(self, expr: Any) -> bool: + """Return True when an expression evaluates to a pointer value.""" + if not isinstance(expr, dict): + return False + if "id" in expr and "func" not in expr: + return str(self.var_types.get(expr["id"], "")).startswith("*") + if "attr" in expr and "value" in expr: + base_type = self.get_expr_struct_type(expr["value"]) + if base_type and base_type in self.struct_defs: + for field in self.struct_defs[base_type].get("fields", []): + if field.get("name") == expr["attr"]: + return str(field.get("type", "")).startswith("*") + return False + def infer_expr_type(self, expr: Any) -> str: """Infer the type of an expression (returns 'i64', 'f64', 'str', 'bool', etc.)""" if expr is None: @@ -499,6 +543,8 @@ def infer_expr_type(self, expr: Any) -> str: return "f64" if expr_type_field in ("string", "str"): return "str" + if expr_type_field == "bytes": + return "bytes" if expr_type_field == "bool": return "bool" @@ -513,6 +559,8 @@ def infer_expr_type(self, expr: Any) -> str: return "f64" if var_type in ("str", "string"): return "str" + if var_type == "bytes": + return "bytes" return "bool" if var_type == "bool" else var_type # Member access (struct field) @@ -564,6 +612,24 @@ def infer_expr_type(self, expr: Any) -> str: elif isinstance(func_info, dict) and "id" in func_info: func_name = func_info["id"] + if func_name in { + "sqrt", "sin", "cos", "tan", "PI", "float", "pow", "str_to_float" + }: + return "f64" + if func_name in {"int", "str_to_int", "round", "floor", "ceil", "len", + "getpid", "getsize"}: + return "i64" + if func_name in {"bool", "exists", "isfile", "isdir", "contains"}: + return "bool" + if func_name == "encode": + return "bytes" + if func_name in {"str", "upper", "lower", "strip", "replace", + "join", "basename", "dirname", "pathjoin", + "getcwd", "abspath", "decode"}: + return "str" + if func_name in {"split", "listdir"}: + return "list" + # Check if we have the function signature from C imports if func_name and func_name in self.c_functions: return_type = self.c_functions[func_name].get("return_type", "i64") @@ -586,6 +652,51 @@ def compile_expr(self, expr: Any, expr_type: str = "i64"): self.emit("CONST_I64 0") return + if isinstance(expr, dict) and expr.get("type") == "addr": + target = expr.get("value") + if isinstance(target, dict) and "id" in target and "func" not in target: + if target["id"] in self.global_vars: + self.emit(f"ADDR_GLOBAL {target['id']}") + else: + self.emit(f"ADDR_LOCAL {self.get_var_id(target['id'])}") + return + if isinstance(target, dict) and "attr" in target and "value" in target: + base = target["value"] + field_name = target["attr"] + struct_type = self.get_expr_struct_type(base) + field_idx = -1 + if struct_type and struct_type in self.struct_defs: + field_idx = self.struct_defs[struct_type]["field_map"].get( + field_name, -1 + ) + if field_idx < 0: + for struct_def in self.struct_defs.values(): + if field_name in struct_def["field_map"]: + field_idx = struct_def["field_map"][field_name] + break + if field_idx < 0: + raise ValueError(f"Unknown field: {field_name}") + self.compile_expr(base, expr_type) + if ( + isinstance(base, dict) + and "id" in base + and str(self.var_types.get(base["id"], "")).startswith("*") + ): + self.emit("DEREF") + self.emit(f"ADDR_FIELD {field_idx}") + return + if isinstance(target, dict) and "slice" in target and "value" in target: + self.compile_expr(target["value"], expr_type) + self.compile_expr(target["slice"], "i64") + self.emit("ADDR_ELEM") + return + raise ValueError("Cannot take the address of this expression") + + if isinstance(expr, dict) and expr.get("type") == "deref": + self.compile_expr(expr.get("value"), expr_type) + self.emit("DEREF") + return + # Literal list (Python list object) if isinstance(expr, list): elements = expr @@ -631,13 +742,13 @@ def compile_expr(self, expr: Any, expr_type: str = "i64"): self.emit(f"LIST_NEW_STR {len(values)} {' '.join(values)}") return - # Create new list using stack values - # Push all elements to stack + # Build the list incrementally. This keeps the bytecode portable + # across every backend (including the C VM) without a special + # stack-pop opcode. + self.emit("LIST_NEW") for elem in elements: self.compile_expr(elem, expr_type) - - # Emit LIST_NEW_STACK count - self.emit(f"LIST_NEW_STACK {len(elements)}") + self.emit("LIST_APPEND") return # Literal boolean (must be before int since bool is subclass of int in Python) @@ -743,13 +854,11 @@ def compile_expr(self, expr: Any, expr_type: str = "i64"): self.emit(f"LIST_NEW_STR {len(values)} {' '.join(values)}") return - # Create new list using stack values - # Push all elements to stack + # Build the list incrementally for backend portability. + self.emit("LIST_NEW") for elem in elements: self.compile_expr(elem, expr_type) - - # Emit LIST_NEW_STACK count - self.emit(f"LIST_NEW_STACK {len(elements)}") + self.emit("LIST_APPEND") return if isinstance(value, int): @@ -909,6 +1018,8 @@ def compile_expr(self, expr: Any, expr_type: str = "i64"): # Compile the struct value (could be a variable, list element, etc.) self.compile_expr(value_node, expr_type) + if self.expr_is_pointer(value_node): + self.emit("DEREF") self.emit(f"STRUCT_GET {field_idx}") return @@ -962,13 +1073,11 @@ def compile_expr(self, expr: Any, expr_type: str = "i64"): self.emit(f"LIST_NEW_STR {len(values)} {' '.join(values)}") return - # Create new list using stack values - # Push all elements to stack + # Build the list incrementally for backend portability. + self.emit("LIST_NEW") for elem in elements: self.compile_expr(elem, expr_type) - - # Emit LIST_NEW_STACK count - self.emit(f"LIST_NEW_STACK {len(elements)}") + self.emit("LIST_APPEND") return # List/Array/Dict indexing (subscript): arr[index] @@ -1118,8 +1227,8 @@ def compile_expr(self, expr: Any, expr_type: str = "i64"): if ( is_string_op or expr_type in {"str", "string"} - or left_type in ("str", "string") - or right_type in ("str", "string") + or left_type in ("str", "string", "bytes") + or right_type in ("str", "string", "bytes") ): type_suffix = "_STR" # If either operand is a float, use float operations @@ -1133,6 +1242,12 @@ def compile_expr(self, expr: Any, expr_type: str = "i64"): else: type_suffix = "_F64" + if type_suffix == "_STR" and op in ("Eq", "==", "NotEq", "!="): + self.emit("STR_EQ") + if op in ("NotEq", "!="): + self.emit("NOT") + return + op_map = { "Add": f"ADD{type_suffix}", "+": f"ADD{type_suffix}", # Support literal '+' operator @@ -1534,6 +1649,15 @@ def compile_statement(self, node: dict, func_return_type: str): node_type = node.get("type") + if node_type == "ptr_assign": + target = node.get("target") + if not (isinstance(target, dict) and target.get("type") == "deref"): + raise ValueError("Invalid pointer assignment target") + self.compile_expr(target.get("value"), None) + self.compile_expr(node.get("value"), None) + self.emit("DEREF_SET") + return + # Variable declaration/assignment if node_type == "var": name = node.get("name", "") @@ -1564,11 +1688,15 @@ def compile_statement(self, node: dict, func_return_type: str): # Static list allocation: list name[capacity:type] = [] list_capacity = node.get("list_capacity") if list_capacity is not None: - if not ( - isinstance(value, dict) - and value.get("type") == "list" - and value.get("value") == [] - ): + empty_list_literal = ( + value == [] + or ( + isinstance(value, dict) + and value.get("type") == "list" + and value.get("value") == [] + ) + ) + if not empty_list_literal: raise CompilerError( "Static lists must be initialized with an empty list literal []" ) @@ -1642,6 +1770,34 @@ def compile_statement(self, node: dict, func_return_type: str): field_name = node.get("field", "") value = node.get("value") + # Parenthesized/dereferenced lvalue: compile the target expression + # directly instead of treating it as a variable name. + if isinstance(target_name, dict): + target_struct = self.get_expr_struct_type(target_name) + field_idx = -1 + if target_struct and target_struct in self.struct_defs: + field_idx = self.struct_defs[target_struct]["field_map"].get( + field_name, -1 + ) + if field_idx < 0: + field_idx = next( + ( + struct_def["field_map"][field_name] + for struct_def in self.struct_defs.values() + if field_name in struct_def["field_map"] + ), + -1, + ) + if field_idx < 0: + raise ValueError(f"Unknown field: {field_name}") + self.compile_expr(target_name, None) + if self.expr_is_pointer(target_name): + self.emit("DEREF") + self.compile_expr(value) + self.emit(f"STRUCT_SET {field_idx}") + self.emit("POP") + return + # Check if target is a Python module import if target_name in self.py_imports: # Python module attribute assignment: ui.debug = False @@ -1684,17 +1840,27 @@ def compile_statement(self, node: dict, func_return_type: str): self.emit("POP") return - # Load the struct + # Load the struct (auto-dereference pointer-typed variables) + is_pointer_target = str(self.var_types.get(target_name, "")).startswith("*") self.emit_load(target_name) - - field_idx = next( - ( - struct_def["field_map"][field_name] - for struct_name, struct_def in self.struct_defs.items() - if field_name in struct_def["field_map"] - ), - -1, - ) + if is_pointer_target: + self.emit("DEREF") + + target_struct = self.get_expr_struct_type({"id": target_name}) + field_idx = -1 + if target_struct and target_struct in self.struct_defs: + field_idx = self.struct_defs[target_struct]["field_map"].get( + field_name, -1 + ) + if field_idx < 0: + field_idx = next( + ( + struct_def["field_map"][field_name] + for struct_def in self.struct_defs.values() + if field_name in struct_def["field_map"] + ), + -1, + ) if field_idx < 0: raise ValueError(f"Unknown field: {field_name}") @@ -1704,8 +1870,13 @@ def compile_statement(self, node: dict, func_return_type: str): # Set field and get modified struct back self.emit(f"STRUCT_SET {field_idx}") - # Store modified struct back - self.emit_store(target_name) + # A pointer target is already a reference: STRUCT_SET mutated it. + # Storing the returned struct into the pointer variable would + # clobber the pointer itself. + if is_pointer_target: + self.emit("POP") + else: + self.emit_store(target_name) elif node_type == "return": value = node.get("value") diff --git a/src/optimizer/__init__.py b/src/optimizer/__init__.py index fa54f36..7970837 100644 --- a/src/optimizer/__init__.py +++ b/src/optimizer/__init__.py @@ -7,7 +7,12 @@ __all__ = ["BytecodeOptimizer", "compile_native_ssa"] -def compile_native_ssa(bytecode: str, opt_level: int = 3, dump_ir: bool = False) -> str: +def compile_native_ssa( + bytecode: str, + opt_level: int = 3, + dump_ir: bool = False, + target: str = "linux", +) -> str: """Compile bytecode to x86_64 assembly using the SSA IR pipeline. This is the new native compilation path: @@ -17,6 +22,7 @@ def compile_native_ssa(bytecode: str, opt_level: int = 3, dump_ir: bool = False) bytecode: The bytecode text (as produced by compiler.py) opt_level: 0=none, 1=basic, 2=standard, 3=aggressive+synthesis dump_ir: If True, return the IR text instead of assembly + target: "linux" or "fr-os" (ABI/ISA profile) Returns: x86_64 assembly text (Intel syntax) or IR text if dump_ir=True @@ -47,7 +53,7 @@ def compile_native_ssa(bytecode: str, opt_level: int = 3, dump_ir: bool = False) pass # z3 not installed, skip # Phase 4: Generate assembly (includes register allocation) - asm = generate_asm(module, opt_level) + asm = generate_asm(module, opt_level, target=target) # Phase 5: Peephole optimize the assembly if opt_level > 0: diff --git a/src/optimizer/codegen.py b/src/optimizer/codegen.py index a7ffecd..f406445 100644 --- a/src/optimizer/codegen.py +++ b/src/optimizer/codegen.py @@ -36,12 +36,74 @@ ) +CALLER_SAVED_XMM = [f"xmm{i}" for i in range(16)] + + +def asm_peephole(text: str) -> str: + """Small post-allocation peephole pass for redundant temp copies.""" + import re + + lines = text.split("\n") + scratch = {f"r{i}" for i in range(8, 12)} | {f"xmm{i}" for i in range(8, 16)} + + def is_reg(s: str) -> bool: + return bool(re.fullmatch(r"(r[0-9]+|xmm[0-9]+)", s)) + + changed = True + while changed: + changed = False + # Count register occurrences globally; a temp used exactly twice is a + # definition plus one consumer and can often be renamed into place. + counts = {r: len(re.findall(rf"\b{r}\b", text)) for r in scratch} + i = 0 + while i + 1 < len(lines): + m = re.match( + r"^(\s*)(lea|mov|movsd|movq)\s+([a-z0-9]+),\s*(.+)$", + lines[i], + ) + m2 = re.match(r"^(\s*)mov(?:sd|q)?\s+([a-z0-9]+),\s*([a-z0-9]+)$", lines[i + 1]) + if m and m2: + _, op, tmp, rest = m.groups() + _, dst, src = m2.groups() + if ( + tmp == src + and tmp in scratch + and counts.get(tmp) == 2 + and is_reg(dst) + and dst != tmp + and not re.search(rf"\b{re.escape(dst)}\b", rest) + ): + lines[i] = f"{m.group(1)}{op} {dst}, {rest}" + del lines[i + 1] + text = "\n".join(lines) + changed = True + break + i += 1 + + # Simple identity cleanup. + cleaned = [] + for line in lines: + st = line.strip() + m = re.match(r"^(\s*)mov\s+([a-z0-9]+),\s*\2$", line) + if m: + continue + m = re.match(r"^(\s*)lea\s+([a-z0-9]+),\s*\[([a-z0-9]+) \+ 0\]$", line) + if m: + cleaned.append(f"{m.group(1)}mov {m.group(2)}, {m.group(3)}") + continue + if re.match(r"^\s*(add|sub)\s+[a-z0-9]+,\s*0$", line): + continue + cleaned.append(line) + return "\n".join(cleaned) + + class CodeGen: """Emit x86_64 assembly from register-allocated IR.""" - def __init__(self, module: Module, opt_level: int = 2): + def __init__(self, module: Module, opt_level: int = 2, target: str = "linux"): self.module = module self.opt_level = opt_level + self.target = target self.lines: list[str] = [] self.string_labels: dict[str, str] = {} self.float_labels: dict[float, str] = {} @@ -103,7 +165,7 @@ def emit(self) -> str: if self._module_needs_bss(): self._emit_bss() - return "\n".join(self.lines) + "\n" + return asm_peephole("\n".join(self.lines)) + "\n" def _emit(self, line: str): self.lines.append(line) @@ -203,8 +265,336 @@ def _emit_phi_copies( # ── Function emission ─────────────────────────────────────── + @staticmethod + def _managed_type(typ) -> bool: + return isinstance(typ, (StructType, ListType, SetType)) + + @staticmethod + def _elem_code(typ) -> int: + if isinstance(typ, (StructType, ListType, SetType)): + return 4 # runtime-managed reference-counted element + if typ == IRType.STRING: + return 1 + if typ == IRType.FLOAT64: + return 2 + if typ == IRType.BOOL: + return 3 + return 0 + + def _lookup_label(self, func: Function) -> str: + safe = "".join(c if c.isalnum() else "_" for c in func.name) + return f"_fr_lut_{safe}" + + def _struct_managed_mask(self, typ) -> int: + if not isinstance(typ, StructType): + return 0 + mask = 0 + for i, field_type in enumerate(typ.field_types[:64]): + if self._managed_type(field_type): + mask |= 1 << i + return mask + + def _emit_lookup_prelude(self, func: Function, alloc: RegAllocation) -> str: + """Emit a table fast path at function entry. Returns the return label.""" + table = func.lookup_table + param = func.params[0] + loc = self._loc(param, alloc) + ret_label = self._new_label("lut_ret") + miss_label = self._new_label("lut_miss") + table_label = self._lookup_label(func) + + if loc.startswith("["): + self._emit(f" mov rax, {loc}") + else: + self._emit(f" mov rax, {loc}") + + if table.get("kind") == "dense": + lo = int(table["min"]) + hi = int(table["max"]) + self._emit(f" cmp rax, {lo}") + self._emit(f" jl {miss_label}") + self._emit(f" cmp rax, {hi}") + self._emit(f" jg {miss_label}") + if lo: + self._emit(f" sub rax, {lo}") + self._emit(f" lea r11, [rip + {table_label}]") + self._emit(" mov rax, [r11 + rax * 8]") + self._emit(f" jmp {ret_label}") + else: + pairs = table.get("pairs") or [] + hit_labels = [] + for i, (arg, _res) in enumerate(pairs): + hit = self._new_label(f"lut_hit{i}") + hit_labels.append(hit) + self._emit(f" cmp rax, {int(arg)}") + self._emit(f" je {hit}") + self._emit(f" jmp {miss_label}") + for (arg, res), hit in zip(pairs, hit_labels): + self._emit(f"{hit}:") + self._emit(f" mov rax, {int(res)}") + self._emit(f" jmp {ret_label}") + + self._emit(f"{miss_label}:") + return ret_label + + def _fold_compare_branches(self, func: Function): + compares = {Op.LT, Op.GT, Op.LE, Op.GE, Op.EQ, Op.NE} + for block in func.blocks: + for i in range(len(block.instructions) - 1): + inst = block.instructions[i] + nxt = block.instructions[i + 1] + if ( + inst.op in compares + and inst.result is not None + and nxt.op == Op.BRANCH + and nxt.operands + and nxt.operands[0] is inst.result + and len(inst.result.uses) == 1 + ): + inst.elided = True + nxt.fused_compare = inst.op + nxt.fused_operands = list(inst.operands) + + def _find_vector_loops(self, func: Function): + """Find simple list-reduction loops suitable for AVX2 vectorization.""" + if self.target != "linux": + # Fr OS carries only the legacy FXSAVE XMM state across a context + # switch; YMM is explicitly forbidden by tools/verify_isa.py. + return {} + from optimizer.analysis import FunctionAnalysis + + try: + loops = FunctionAnalysis(func).loops.loops + except Exception: + return {} + plans = {} + for loop in loops: + header = loop.header + term = header.terminator + if term is None or term.op != Op.BRANCH or len(term.target_blocks) < 2: + continue + if not term.operands: + continue + cond = term.operands[0] + ci = cond.defining_inst + if ci is None or ci.op != Op.LT or len(ci.operands) < 2: + continue + index_phi = ci.operands[0] if isinstance(ci.operands[0], Value) else None + length_val = ci.operands[1] if isinstance(ci.operands[1], Value) else None + if index_phi is None or length_val is None or length_val.defining_inst is None: + continue + if length_val.defining_inst.op != Op.LIST_LEN: + continue + if index_phi.defining_inst is None or index_phi.defining_inst.op != Op.PHI: + continue + # The generated vector loop scans from index zero. Only accept a + # loop whose phi starts at a literal zero. + from optimizer.passes import _get_const_value + outside_init = None + for operand, pred in zip( + index_phi.defining_inst.operands, + index_phi.defining_inst.target_blocks, + ): + if pred not in loop.blocks: + outside_init = operand + break + if outside_init is None or _get_const_value(outside_init) != 0: + continue + list_val = length_val.defining_inst.operands[0] + if not isinstance(list_val, Value): + continue + if not header.phi_nodes or len(header.phi_nodes) > 3: + continue + acc_phi = None + for phi in header.phi_nodes: + if phi.result is not None and phi.result is not index_phi: + acc_phi = phi + break + if acc_phi is None or acc_phi.result is None: + continue + if exit_block := term.target_blocks[1]: + if exit_block.phi_nodes: + continue + + list_get = None + acc_next = None + index_next = None + for block in loop.blocks: + for inst in block.instructions: + if inst.op == Op.LIST_GET and len(inst.operands) >= 2: + if inst.operands[0] is list_val and inst.operands[1] is index_phi: + list_get = inst + if inst.op in (Op.ADD, Op.FADD) and len(inst.operands) >= 2: + if acc_phi.result in inst.operands and inst.result is not None: + acc_next = inst + if inst.op == Op.ADD and len(inst.operands) >= 2: + from optimizer.passes import _get_const_value + if inst.operands[0] is index_phi: + c = _get_const_value(inst.operands[1]) + if c == 1: + index_next = inst + if list_get is None or acc_next is None or index_next is None: + continue + + # Only allow the simple reduction body; reject other effects. + allowed = { + Op.PHI, Op.LIST_LEN, Op.LT, Op.LIST_GET, Op.ADD, Op.FADD, + Op.FMUL, Op.CONST_INT, Op.CONST_FLOAT, Op.JUMP, Op.BRANCH, + } + if any(inst.op not in allowed for block in loop.blocks for inst in block.instructions): + continue + + contribution = acc_next.operands[0] if acc_next.operands[1] is acc_phi.result else acc_next.operands[1] + kind = None + if acc_next.op == Op.ADD and contribution is list_get.result: + kind = "sum_i" + elif acc_next.op == Op.FADD and contribution is list_get.result: + kind = "sum_f" + elif acc_next.op == Op.FADD and isinstance(contribution, Value) and contribution.defining_inst is not None: + mul = contribution.defining_inst + if mul.op == Op.FMUL and len(mul.operands) >= 2 and all( + op is list_get.result for op in mul.operands + ): + kind = "sumsq_f" + if kind is None: + continue + elem = list_val.type.elem_type if isinstance(list_val.type, ListType) else None + if kind == "sum_i" and elem != IRType.INT64: + continue + if kind in ("sum_f", "sumsq_f") and elem != IRType.FLOAT64: + continue + plans[header] = { + "header": header, + "exit": term.target_blocks[1], + "loop_blocks": list(loop.blocks), + "list": list_val, + "acc": acc_phi, + "kind": kind, + } + return plans + + def _emit_vector_loop(self, func: Function, plan, alloc: RegAllocation): + header = plan["header"] + exit_block = plan["exit"] + list_loc = self._loc(plan["list"], alloc) + acc_loc = self._loc(plan["acc"].result, alloc) + kind = plan["kind"] + is_float = kind in ("sum_f", "sumsq_f") + asm_name = self._asm_func_name(func.name) + vec = self._new_label("vec") + scalar = self._new_label("vec_scalar") + horiz = self._new_label("vec_done") + done = self._new_label("vec_exit") + + self._emit(f".{asm_name}_{header.label}:") + self._emit(" sub rsp, 560") + for i in range(16): + self._emit(f" vmovups [rsp + {i * 32}], ymm{i}") + self._emit(" mov [rsp + 512], rax") + self._emit(" mov [rsp + 520], r10") + self._emit(" mov [rsp + 528], r11") + self._emit(" mov [rsp + 536], rdx") + + # Preserve the initial accumulator value. + if is_float: + if acc_loc.startswith("xmm"): + self._emit(f" movsd [rsp + 544], {acc_loc}") + else: + self._emit(f" movq xmm15, {acc_loc}" if acc_loc.startswith("[") else f" movq xmm15, {acc_loc}") + self._emit(" movsd [rsp + 544], xmm15") + self._emit(" vxorpd ymm0, ymm0, ymm0") + self._emit(" vxorpd xmm2, xmm2, xmm2") + else: + if acc_loc.startswith("["): + self._emit(f" mov rax, {acc_loc}") + self._emit(" mov [rsp + 544], rax") + else: + self._emit(f" mov [rsp + 544], {acc_loc}") + self._emit(" vpxor ymm0, ymm0, ymm0") + self._emit(" vpxor xmm2, xmm2, xmm2") + + if list_loc.startswith("["): + self._emit(f" mov r11, {list_loc}") + else: + self._emit(f" mov r11, {list_loc}") + self._emit(" mov r10, [r11 + 8]") + self._emit(" mov r11, [r11]") + self._emit(" xor eax, eax") + self._emit(f"{vec}:") + self._emit(" lea rdx, [rax + 4]") + self._emit(" cmp rdx, r10") + self._emit(f" ja {scalar}") + self._emit(" vmovupd ymm1, [r11 + rax * 8]") + if kind == "sumsq_f": + self._emit(" vmulpd ymm1, ymm1, ymm1") + self._emit( + " vpaddq ymm0, ymm0, ymm1" if not is_float else " vaddpd ymm0, ymm0, ymm1" + ) + self._emit(" add rax, 4") + self._emit(f" jmp {vec}") + self._emit(f"{scalar}:") + self._emit(" cmp rax, r10") + self._emit(f" jae {horiz}") + if is_float: + if kind == "sumsq_f": + self._emit(" vmovsd xmm3, [r11 + rax * 8]") + self._emit(" vmulsd xmm3, xmm3, xmm3") + self._emit(" vaddsd xmm2, xmm2, xmm3") + else: + self._emit(" vaddsd xmm2, xmm2, [r11 + rax * 8]") + else: + self._emit(" mov rdx, [r11 + rax * 8]") + self._emit(" vmovq xmm3, rdx") + self._emit(" vpaddq xmm2, xmm2, xmm3") + self._emit(" inc rax") + self._emit(f" jmp {scalar}") + self._emit(f"{horiz}:") + if is_float: + self._emit(" vextractf128 xmm1, ymm0, 1") + self._emit(" vaddpd xmm0, xmm0, xmm1") + self._emit(" vshufpd xmm1, xmm0, xmm0, 1") + self._emit(" vaddpd xmm0, xmm0, xmm1") + self._emit(" vaddsd xmm0, xmm0, xmm2") + self._emit(" vaddsd xmm0, xmm0, [rsp + 544]") + self._emit(" movsd [rsp + 552], xmm0") + else: + self._emit(" vextracti128 xmm1, ymm0, 1") + self._emit(" vpaddq xmm0, xmm0, xmm1") + self._emit(" vpshufd xmm1, xmm0, 0x4E") + self._emit(" vpaddq xmm0, xmm0, xmm1") + self._emit(" vmovq rax, xmm0") + self._emit(" vmovq rdx, xmm2") + self._emit(" add rax, rdx") + self._emit(" add rax, [rsp + 544]") + self._emit(" mov [rsp + 552], rax") + + for i in range(16): + self._emit(f" vmovups ymm{i}, [rsp + {i * 32}]") + self._emit(" mov rax, [rsp + 512]") + self._emit(" mov r10, [rsp + 520]") + self._emit(" mov r11, [rsp + 528]") + self._emit(" mov rdx, [rsp + 536]") + self._emit(" vzeroupper") + if is_float: + if acc_loc.startswith("xmm"): + self._emit(f" movsd {acc_loc}, [rsp + 552]") + elif acc_loc.startswith("["): + self._emit(f" movsd xmm15, [rsp + 552]") + self._emit(f" movsd {acc_loc}, xmm15") + else: + self._emit(f" movq {acc_loc}, [rsp + 552]") + else: + if acc_loc.startswith("["): + self._emit(f" mov rdx, [rsp + 552]") + self._emit(f" mov {acc_loc}, rdx") + else: + self._emit(f" mov {acc_loc}, [rsp + 552]") + self._emit(" add rsp, 560") + self._emit(f" jmp .{asm_name}_{exit_block.label}") + def _emit_function(self, func: Function): """Emit a complete function with prologue, body, epilogue.""" + self._fold_compare_branches(func) alloc = allocate_registers(func) asm_name = self._asm_func_name(func.name) @@ -212,12 +602,26 @@ def _emit_function(self, func: Function): self._compute_caller_saves(func, alloc) callee_save_size = len(alloc.used_callee_saved) * 8 - has_calls = self._func_has_calls(func) + needs_scope = self._func_needs_scope(func) + has_calls = self._func_has_calls(func) or needs_scope has_spills = alloc.frame_size > 0 has_callee_saves = callee_save_size > 0 + # Reserve fixed stack slots for non-escaping struct allocations. + self._stack_alloc_offsets: dict[int, int] = {} + stack_bytes = 0 + stack_base = alloc.frame_size + callee_save_size + for inst in func.all_instructions(): + if inst.op == Op.ALLOC_STRUCT and inst.stack_alloc: + size = max(8, len(inst.operands) * 8) + size = (size + 7) & ~7 + stack_bytes += size + self._stack_alloc_offsets[id(inst)] = stack_base + stack_bytes + if stack_bytes: + has_spills = True + if has_spills: - total_frame = alloc.frame_size + callee_save_size + total_frame = alloc.frame_size + callee_save_size + stack_bytes total_frame = (total_frame + 15) & ~15 elif has_callee_saves: total_frame = callee_save_size @@ -248,14 +652,36 @@ def _emit_function(self, func: Function): elif has_callee_saves: for reg in alloc.used_callee_saved: self._emit(f" push {reg}") - if has_calls: + # Re-establish 16-byte alignment before calls. After the pushes + # rsp is 0 mod 16 when an odd number of registers was saved. + if has_calls and (callee_save_size % 16) == 0: self._emit(" sub rsp, 8") elif has_calls: self._emit(" sub rsp, 8") + if needs_scope: + self._emit_call_aligned("runtime_scope_push") + + lookup_ret = None + if func.lookup_table and func.params: + lookup_ret = self._emit_lookup_prelude(func, alloc) + + vector_plans = self._find_vector_loops(func) + skipped: set[int] = set() + for plan in vector_plans.values(): + for lb in plan["loop_blocks"]: + skipped.add(id(lb)) for block in func.blocks: + if id(block) in skipped: + if block in vector_plans: + self._emit_vector_loop(func, vector_plans[block], alloc) + continue self._emit_block(func, block, alloc) + if lookup_ret is not None: + self._emit(f"{lookup_ret}:") + self._emit_epilogue(func, alloc) + self._emit("") # ── Call-site analysis: caller-saved registers ──────────────── @@ -273,12 +699,14 @@ def _emit_function(self, func: Function): Op.LIST_GET, Op.LIST_SET, Op.LIST_APPEND, - Op.LIST_LEN, Op.ALLOC_STRUCT, + Op.ALLOCA, + Op.LIST_ELEM_ADDR, Op.STR_CONCAT, Op.TO_STR, - Op.TO_BOOL, - Op.SQRT, + Op.STORE_FIELD, + Op.STORE_GLOBAL, + Op.PTR_STORE, Op.SIN, Op.COS, Op.TAN, @@ -300,10 +728,10 @@ def _compute_caller_saves(self, func: Function, alloc: RegAllocation): hold values that are live across the call and must be pushed/popped.""" caller_saved_set = frozenset(CALLER_SAVED_GPRS) - # Build a mapping: value_id → assigned register (only caller-saved) + # Build a mapping: value_id → assigned register (caller-saved GPR/XMM) val_to_cs_reg: dict[int, str] = {} for vid, reg in alloc.reg_map.items(): - if reg in caller_saved_set: + if reg in caller_saved_set or reg in CALLER_SAVED_XMM: val_to_cs_reg[vid] = reg if not val_to_cs_reg: @@ -333,11 +761,15 @@ def _compute_caller_saves(self, func: Function, alloc: RegAllocation): for iv in intervals: if iv.value.id not in val_to_cs_reg: continue - # Value must be defined before this call and used after - if iv.start < cp and iv.end > cp + 1: + # A value defined by this call is not live before it; + # everything else live across the call must be preserved. + defining = getattr(iv.value, "defining_inst", None) + if defining is inst: + continue + if iv.start <= cp and iv.end > cp: reg = val_to_cs_reg[iv.value.id] - # Don't save argument registers that are being passed - # to this call (they're already set up) + if inst.op == Op.LIST_GET and reg not in ("rax", "r10", "r11"): + continue if reg not in save_regs: save_regs.append(reg) @@ -354,15 +786,36 @@ def _emit_block(self, func: Function, block: BasicBlock, alloc: RegAllocation): self._emit(f".{asm_name}_{block.label}:") for inst in block.instructions: + if getattr(inst, "elided", False): + continue # Save/restore caller-saved registers around call instructions save_regs = ( self._get_caller_saves(inst) if inst.op in self._CALL_OPS else [] ) - for reg in save_regs: - self._emit(f" push {reg}") + save_bytes = 0 + gprs = [r for r in save_regs if not r.startswith("xmm")] + xmms = [r for r in save_regs if r.startswith("xmm")] + save_bytes = len(gprs) * 8 + len(xmms) * 8 + save_bytes = (save_bytes + 15) & ~15 + if save_bytes: + self._emit(f" sub rsp, {save_bytes}") + off = 0 + for reg in gprs: + self._emit(f" mov [rsp + {off}], {reg}") + off += 8 + for reg in xmms: + self._emit(f" movsd [rsp + {off}], {reg}") + off += 8 self._emit_instruction(func, block, inst, alloc) - for reg in reversed(save_regs): - self._emit(f" pop {reg}") + if save_bytes: + off = 0 + for reg in gprs: + self._emit(f" mov {reg}, [rsp + {off}]") + off += 8 + for reg in xmms: + self._emit(f" movsd {reg}, [rsp + {off}]") + off += 8 + self._emit(f" add rsp, {save_bytes}") # If this block doesn't end with a terminator (falls through to next), # emit PHI copies for the fall-through successor @@ -389,7 +842,10 @@ def _emit_instruction( else: self._emit(f" xor {_dword(dst)}, {_dword(dst)}") else: - self._emit(f" mov {dst}, {val}") + if dst.startswith("["): + self._emit(f" mov qword ptr {dst}, {val}") + else: + self._emit(f" mov {dst}, {val}") return if op == Op.CONST_FLOAT: @@ -406,16 +862,22 @@ def _emit_instruction( if op == Op.CONST_STR: dst = self._loc(inst.result, alloc) label = self._get_string_label(inst.imm_str or "") - self._emit(f" lea {dst}, [rip + {label}]") + self._emit_lea(dst, f"[rip + {label}]") return if op == Op.CONST_BOOL: dst = self._loc(inst.result, alloc) val = inst.imm_int or 0 if val == 0: - self._emit(f" xor {_dword(dst)}, {_dword(dst)}") + if dst.startswith("["): + self._emit(f" mov qword ptr {dst}, 0") + else: + self._emit(f" xor {_dword(dst)}, {_dword(dst)}") else: - self._emit(f" mov {dst}, 1") + if dst.startswith("["): + self._emit(f" mov qword ptr {dst}, 1") + else: + self._emit(f" mov {dst}, 1") return # ── Integer arithmetic ── @@ -447,6 +909,15 @@ def _emit_instruction( dst = self._loc(inst.result, alloc) a = self._loc(inst.operands[0], alloc) b = self._loc(inst.operands[1], alloc) + if not getattr(inst, "no_zero_check", False): + skip = self._new_label("div_ok") + self._emit(f" test {b}, {b}") + self._emit(f" jne {skip}") + msg = self._get_string_label("integer division by zero") + self._emit(f" lea rdi, [rip + {msg}]") + self._emit(f" mov esi, {inst.source_line or 0}") + self._emit_call_aligned("runtime_error_at") + self._emit(f"{skip}:") self._emit(f" mov rax, {a}") self._emit(f" cqo") # Can't idiv by memory with just register @@ -466,6 +937,15 @@ def _emit_instruction( dst = self._loc(inst.result, alloc) a = self._loc(inst.operands[0], alloc) b = self._loc(inst.operands[1], alloc) + if not getattr(inst, "no_zero_check", False): + skip = self._new_label("mod_ok") + self._emit(f" test {b}, {b}") + self._emit(f" jne {skip}") + msg = self._get_string_label("integer division by zero") + self._emit(f" lea rdi, [rip + {msg}]") + self._emit(f" mov esi, {inst.source_line or 0}") + self._emit_call_aligned("runtime_error_at") + self._emit(f"{skip}:") self._emit(f" mov rax, {a}") self._emit(f" cqo") if b.startswith("[") or b in ("rax", "rdx"): @@ -479,6 +959,19 @@ def _emit_instruction( if op == Op.NEG: dst = self._loc(inst.result, alloc) a = self._loc(inst.operands[0], alloc) + if is_float_type(inst.result.type) or ( + inst.operands and is_float_type(inst.operands[0].type) + ): + a_xmm = a if a.startswith("xmm") else "xmm14" + if not a.startswith("xmm"): + self._emit(f" movq {a_xmm}, {a}") + self._emit(" xorpd xmm15, xmm15") + self._emit(f" subsd xmm15, {a_xmm}") + if dst.startswith("xmm"): + self._emit(f" movsd {dst}, xmm15") + else: + self._emit(f" movq {dst}, xmm15") + return if dst != a: self._emit(f" mov {dst}, {a}") self._emit(f" neg {dst}") @@ -496,23 +989,24 @@ def _emit_instruction( self._emit_float_binop(inst, alloc, "mulsd") return if op == Op.FDIV: - # Check for division by zero — raise exception if divisor is 0.0 - b = self._loc(inst.operands[1], alloc) - skip_label = self._new_label("fdiv_ok") - self._emit(f" xorpd xmm15, xmm15") - if b.startswith("xmm"): - self._emit(f" ucomisd {b}, xmm15") - else: - self._emit(f" movq xmm14, {b}") - self._emit(f" ucomisd xmm14, xmm15") - self._emit(f" jne {skip_label}") - # Divisor is zero — raise ZeroDivisionError - zdstr = self._get_string_label("ZeroDivisionError") - msgstr = self._get_string_label("float division by zero") - self._emit(f" lea rdi, [rip + {zdstr}]") - self._emit(f" lea rsi, [rip + {msgstr}]") - self._emit_call_aligned("runtime_exception_raise") - self._emit(f"{skip_label}:") + if not getattr(inst, "no_zero_check", False): + # Check for division by zero — raise exception if divisor is 0.0 + b = self._loc(inst.operands[1], alloc) + skip_label = self._new_label("fdiv_ok") + zero_label = self._get_float_label(0.0) + if b.startswith("xmm"): + self._emit(f" ucomisd {b}, [rip + {zero_label}]") + else: + self._emit(f" movq xmm14, {b}") + self._emit(f" ucomisd xmm14, [rip + {zero_label}]") + self._emit(f" jne {skip_label}") + # Divisor is zero — report through runtime_error_at so handlers and + # test-mode error formatting behave exactly like the other VMs. + msgstr = self._get_string_label("float division by zero") + self._emit(f" lea rdi, [rip + {msgstr}]") + self._emit(f" mov esi, {inst.source_line or 0}") + self._emit_call_aligned("runtime_error_at") + self._emit(f"{skip_label}:") self._emit_float_binop(inst, alloc, "divsd") return if op == Op.FNEG: @@ -562,7 +1056,11 @@ def _emit_instruction( if op == Op.NOT: dst = self._loc(inst.result, alloc) a = self._loc(inst.operands[0], alloc) - self._emit(f" test {a}, {a}") + if a.startswith("["): + self._emit(f" mov r11, {a}") + self._emit(" test r11, r11") + else: + self._emit(f" test {a}, {a}") self._emit(f" setz al") self._emit(f" movzx {dst}, al") return @@ -649,29 +1147,154 @@ def _emit_instruction( else: base_ref = base if dst.startswith("xmm"): - self._emit(f" movsd {dst}, [{base_ref} + {offset}]") + if dst.startswith("["): + self._emit(f" movsd xmm15, [{base_ref} + {offset}]") + self._emit(f" movsd {dst}, xmm15") + else: + self._emit(f" movsd {dst}, [{base_ref} + {offset}]") else: - self._emit(f" mov {dst}, [{base_ref} + {offset}]") + self._emit_int_move(f"[{base_ref} + {offset}]", dst) return if op == Op.STORE_FIELD: base = self._loc(inst.operands[0], alloc) val = self._loc(inst.operands[1], alloc) if len(inst.operands) > 1 else "0" offset = (inst.imm_int or 0) * 8 + struct_type = inst.operands[0].type if inst.operands else None + field_idx = inst.imm_int or 0 + managed = ( + isinstance(struct_type, StructType) + and field_idx < len(struct_type.field_types) + and self._managed_type(struct_type.field_types[field_idx]) + ) + if managed: + self._emit(" sub rsp, 32") + self._emit(f" mov rax, {base}") + self._emit(" mov [rsp], rax") + self._emit(f" mov rax, {val}") + self._emit(" mov [rsp + 8], rax") + self._emit(" mov rdi, [rsp]") + self._emit(" mov rsi, [rsp + 8]") + self._emit(f" mov rdx, {field_idx}") + self._emit(" mov rcx, 1") + self._emit_call_aligned("runtime_store_field") + self._emit(" add rsp, 32") + return if base.startswith("["): self._emit(f" mov r11, {base}") base_ref = "r11" else: base_ref = base if val.startswith("xmm"): - self._emit(f" movsd [{base_ref} + {offset}], {val}") + if val.startswith("["): + self._emit(f" movsd xmm15, {val}") + self._emit(f" movsd [{base_ref} + {offset}], xmm15") + else: + self._emit(f" movsd [{base_ref} + {offset}], {val}") else: + if val.startswith("["): + self._emit(f" mov r10, {val}") + val = "r10" self._emit(f" mov [{base_ref} + {offset}], {val}") return + # ── Pointer operations ── + + if op == Op.ADDR_GLOBAL: + dst = self._loc(inst.result, alloc) + name = inst.imm_str or "unknown" + self._emit_lea(dst, f"[rip + _fr_global_{name}]") + self.module.global_vars[name] = IRType.INT64 + return + + if op == Op.ALLOCA: + dst = self._loc(inst.result, alloc) + self._emit(" mov rdi, 8") + self._emit_call_aligned("runtime_alloc") + if dst != "rax": + self._emit(f" mov {dst}, rax") + return + + if op == Op.PTR_LOAD: + dst = self._loc(inst.result, alloc) + ptr = self._loc(inst.operands[0], alloc) + if ptr.startswith("["): + self._emit(f" mov r11, {ptr}") + ptr_ref = "r11" + else: + ptr_ref = ptr + if dst.startswith("xmm"): + if dst.startswith("["): + self._emit(f" movsd xmm15, [{ptr_ref}]") + self._emit(f" movsd {dst}, xmm15") + else: + self._emit(f" movsd {dst}, [{ptr_ref}]") + else: + self._emit_int_move(f"[{ptr_ref}]", dst) + return + + if op == Op.PTR_STORE: + ptr = self._loc(inst.operands[0], alloc) + val = self._loc(inst.operands[1], alloc) + ptr_type = inst.operands[0].type if inst.operands else None + val_type = inst.operands[1].type if len(inst.operands) > 1 else None + pointee = getattr(ptr_type, "pointee", None) + managed = self._managed_type(val_type) and self._managed_type(pointee) + if managed: + self._emit(" sub rsp, 32") + self._emit(f" mov rax, {ptr}") + self._emit(" mov [rsp], rax") + self._emit(f" mov rax, {val}") + self._emit(" mov [rsp + 8], rax") + self._emit(" mov rdi, [rsp]") + self._emit(" mov rsi, [rsp + 8]") + self._emit(" mov rdx, 1") + self._emit_call_aligned("runtime_ptr_store") + self._emit(" add rsp, 32") + return + if ptr.startswith("["): + self._emit(f" mov r11, {ptr}") + ptr_ref = "r11" + else: + ptr_ref = ptr + if val.startswith("xmm"): + if val.startswith("["): + self._emit(f" movsd xmm15, {val}") + self._emit(f" movsd [{ptr_ref}], xmm15") + else: + self._emit(f" movsd [{ptr_ref}], {val}") + else: + if val.startswith("["): + self._emit(f" mov r10, {val}") + val = "r10" + self._emit(f" mov [{ptr_ref}], {val}") + return + + if op == Op.FIELD_ADDR: + dst = self._loc(inst.result, alloc) + base = self._loc(inst.operands[0], alloc) + offset = (inst.imm_int or 0) * 8 + if base.startswith("["): + self._emit(f" mov r11, {base}") + base_ref = "r11" + else: + base_ref = base + self._emit_lea(dst, f"[{base_ref} + {offset}]") + return + + if op == Op.LIST_ELEM_ADDR: + self._emit_runtime_call("runtime_list_elem_addr", inst, alloc, 2) + return + # ── List operations ── if op == Op.ALLOC_LIST: + elem_type = ( + inst.result.type.elem_type + if isinstance(inst.result.type, ListType) + else IRType.INT64 + ) + elem_code = self._elem_code(elem_type) if inst.operands: # List with initial elements — allocate array space on stack, # store elements, call runtime, then free the space. @@ -682,41 +1305,90 @@ def _emit_instruction( self._emit(f" sub rsp, {aligned_size}") for i, v in enumerate(inst.operands): loc = self._loc(v, alloc) - self._emit(f" mov [rsp + {i * 8}], {loc}") + if loc.startswith("xmm"): + self._emit(f" movsd [rsp + {i * 8}], {loc}") + else: + self._emit(f" mov [rsp + {i * 8}], {loc}") self._emit(f" mov rdi, rsp") self._emit(f" mov rsi, {n}") - self._emit_call_aligned("runtime_list_from_array") + self._emit(f" mov rdx, {elem_code}") + self._emit_call_aligned("runtime_list_from_array_typed") self._emit(f" add rsp, {aligned_size}") else: - self._emit_call_aligned("runtime_list_new") + self._emit(f" mov rdi, {elem_code}") + self._emit_call_aligned("runtime_list_new_typed") dst = self._loc(inst.result, alloc) if dst != "rax": self._emit(f" mov {dst}, rax") return if op == Op.LIST_APPEND: - self._emit_runtime_call("runtime_list_append_int", inst, alloc, 2) + managed = len(inst.operands) > 1 and self._managed_type( + inst.operands[1].type + ) + self._emit_runtime_call( + "runtime_list_append_managed" if managed + else "runtime_list_append_int", + inst, + alloc, + 2, + ) return if op == Op.LIST_GET: - # Check if indexing a string → use runtime_str_get_char list_type = inst.operands[0].type if inst.operands else None if list_type == IRType.STRING: self._emit_runtime_call("runtime_str_get_char", inst, alloc, 2) else: + dst = self._loc(inst.result, alloc) if inst.result else "rax" a = self._loc(inst.operands[0], alloc) b = self._loc(inst.operands[1], alloc) line = inst.source_line or 0 - if a != "rdi": - self._emit(f" mov rdi, {a}") - if b != "rsi": - self._emit(f" mov rsi, {b}") + slow = self._new_label("list_get_slow") + done = self._new_label("list_get_done") + # Stage operands so loading scratch registers never clobbers + # a list/index that happened to live in r10/r11/rax. + self._emit(" sub rsp, 16") + if a.startswith("["): + self._emit(f" mov rax, {a}") + self._emit(" mov [rsp], rax") + else: + self._emit(f" mov [rsp], {a}") + if b.startswith("["): + self._emit(f" mov rax, {b}") + self._emit(" mov [rsp + 8], rax") + else: + self._emit(f" mov [rsp + 8], {b}") + self._emit(" mov r11, [rsp]") + self._emit(" mov r10, [r11 + 8]") + self._emit(" mov rax, [rsp + 8]") + self._emit(" test rax, rax") + self._emit(f" js {slow}") + self._emit(" cmp rax, r10") + self._emit(f" jae {slow}") + self._emit(" mov r11, [r11]") + if dst.startswith("xmm"): + self._emit(f" movq {dst}, [r11 + rax * 8]") + elif dst.startswith("["): + self._emit(f" mov r10, [r11 + rax * 8]") + self._emit(f" mov {dst}, r10") + else: + self._emit(f" mov {dst}, [r11 + rax * 8]") + self._emit(" add rsp, 16") + self._emit(f" jmp {done}") + self._emit(f"{slow}:") + self._emit(" mov rdi, [rsp]") + self._emit(" mov rsi, [rsp + 8]") self._emit(f" mov rdx, {line}") self._emit_call_aligned("runtime_list_get_int_at") - if inst.result: - dst = self._loc(inst.result, alloc) - if dst != "rax": - self._emit(f" mov {dst}, rax") + if dst.startswith("xmm"): + self._emit(f" movq {dst}, rax") + elif dst.startswith("["): + self._emit(f" mov {dst}, rax") + elif dst != "rax": + self._emit(f" mov {dst}, rax") + self._emit(" add rsp, 16") + self._emit(f"{done}:") return if op == Op.LIST_SET: @@ -724,6 +1396,9 @@ def _emit_instruction( b = self._loc(inst.operands[1], alloc) c = self._loc(inst.operands[2], alloc) line = inst.source_line or 0 + managed = len(inst.operands) > 2 and self._managed_type( + inst.operands[2].type + ) if a != "rdi": self._emit(f" mov rdi, {a}") if b != "rsi": @@ -731,11 +1406,33 @@ def _emit_instruction( if c != "rdx": self._emit(f" mov rdx, {c}") self._emit(f" mov rcx, {line}") - self._emit_call_aligned("runtime_list_set_int_at") + self._emit_call_aligned( + "runtime_list_set_managed_at" if managed + else "runtime_list_set_int_at" + ) return if op == Op.LIST_LEN: - self._emit_runtime_call("runtime_list_len", inst, alloc, 1) + dst = self._loc(inst.result, alloc) + lst = self._loc(inst.operands[0], alloc) + if dst.startswith("xmm"): + # Unusual: length is integer but the allocator picked an XMM. + self._emit(f" mov rax, {lst}" if lst.startswith("[") else f" mov rax, {lst}") + self._emit(" mov rax, [rax + 8]") + self._emit(f" movq {dst}, rax") + elif lst.startswith("["): + if dst.startswith("["): + self._emit(f" mov r11, {lst}") + self._emit(" mov r10, [r11 + 8]") + self._emit(f" mov {dst}, r10") + else: + # dst doubles as the temporary list pointer: no scratch clobber. + self._emit_int_move(lst, dst) + self._emit(f" mov {dst}, [{dst} + 8]") + elif dst == lst: + self._emit(f" mov {dst}, [{dst} + 8]") + else: + self._emit(f" mov {dst}, [{lst} + 8]") return # ── Control flow ── @@ -751,11 +1448,41 @@ def _emit_instruction( return if op == Op.BRANCH: - cond = self._loc(inst.operands[0], alloc) - self._emit(f" test {cond}, {cond}") - # Branch: if false → jump to false label, else fall through to next block false_label = inst.imm_str asm_name = self._asm_func_name(func.name) + if inst.fused_compare is not None and inst.fused_operands: + a = self._loc(inst.fused_operands[0], alloc) + b = self._loc(inst.fused_operands[1], alloc) + a_type = inst.fused_operands[0].type + b_type = inst.fused_operands[1].type + is_float = is_float_type(a_type) or is_float_type(b_type) + if is_float: + a_xmm = a if a.startswith("xmm") else "xmm14" + b_xmm = b if b.startswith("xmm") else "xmm15" + if not a.startswith("xmm"): + self._emit(f" movq {a_xmm}, {a}") + if not b.startswith("xmm"): + self._emit(f" movq {b_xmm}, {b}") + self._emit(f" ucomisd {a_xmm}, {b_xmm}") + inv = { + Op.LT: "jae", Op.GT: "jbe", Op.LE: "ja", + Op.GE: "jb", Op.EQ: "jne", Op.NE: "je", + } + else: + self._emit(f" cmp {a}, {b}") + inv = { + Op.LT: "jge", Op.GT: "jle", Op.LE: "jg", + Op.GE: "jl", Op.EQ: "jne", Op.NE: "je", + } + branch_cc = inv.get(inst.fused_compare, "je") + else: + cond = self._loc(inst.operands[0], alloc) + if cond.startswith("["): + self._emit(f" mov rax, {cond}") + self._emit(" test rax, rax") + else: + self._emit(f" test {cond}, {cond}") + branch_cc = "jz" # We need PHI copies for the false branch (jumped to) and the # true branch (fall-through to the next block). # For the false branch, emit copies before the jz: @@ -765,7 +1492,7 @@ def _emit_instruction( # Use a trampoline: jump to a copy block, then to target tramp_label = f".{asm_name}_phi_tramp_{self._next_label()}" done_label = f".{asm_name}_phi_done_{self._next_label()}" - self._emit(f" jz {tramp_label}") + self._emit(f" {branch_cc} {tramp_label}") # Fall-through (true) path — emit PHI copies for the next block true_block = self._get_fall_through_block(func, block) if true_block: @@ -776,7 +1503,7 @@ def _emit_instruction( self._emit(f" jmp .{asm_name}_{false_label}") self._emit(f"{done_label}:") else: - self._emit(f" jz .{asm_name}_{false_label}") + self._emit(f" {branch_cc} .{asm_name}_{false_label}") # Fall-through (true) path true_block = self._get_fall_through_block(func, block) if true_block: @@ -795,6 +1522,16 @@ def _emit_instruction( else: if val != "rax": self._emit(f" mov rax, {val}") + # Returning a managed value transfers a reference to the caller. + if self._managed_type(func.return_type) or self._managed_type(val_type): + self._emit(" sub rsp, 16") + self._emit(" mov rdi, rax") + self._emit(" mov [rsp], rax") + self._emit(" movsd [rsp + 8], xmm0") + self._emit_call_aligned("runtime_retain_transfer") + self._emit(" mov rax, [rsp]") + self._emit(" movsd xmm0, [rsp + 8]") + self._emit(" add rsp, 16") self._emit_epilogue(func, alloc) return @@ -862,18 +1599,17 @@ def _emit_instruction( a = self._loc(inst.operands[0], alloc) if a != "rdi": self._emit(f" mov rdi, {a}") + self._emit(" mov rsi, 1") self._emit_call_aligned("fputs") - self._emit(" mov rdi, stdout") - self._emit_call_aligned("fflush") elif val_type == IRType.FLOAT64: a = self._loc(inst.operands[0], alloc) - fmt = self._get_format_label("%f") + fmt = self._get_format_label("%g") self._emit(f" lea rdi, [rip + {fmt}]") if a.startswith("xmm"): - self._emit(f" movsd xmm1, {a}") + self._emit(f" movsd xmm0, {a}") else: - self._emit(f" movq xmm1, {a}") - self._emit(" mov rsi, rdi") + self._emit(f" movq xmm0, {a}") + self._emit(" mov eax, 1") self._emit_call_aligned("printf") elif val_type == IRType.BOOL: a = self._loc(inst.operands[0], alloc) @@ -881,32 +1617,30 @@ def _emit_instruction( self._emit(f" mov rdi, {a}") self._emit_call_aligned("runtime_bool_to_str") self._emit(f" mov rdi, rax") + self._emit(" mov rsi, 1") self._emit_call_aligned("fputs") - self._emit(" mov rdi, stdout") - self._emit_call_aligned("fflush") elif isinstance(val_type, ListType): a = self._loc(inst.operands[0], alloc) if a != "rdi": self._emit(f" mov rdi, {a}") self._emit_call_aligned("runtime_list_to_str") self._emit(f" mov rdi, rax") + self._emit(" mov rsi, 1") self._emit_call_aligned("fputs") - self._emit(" mov rdi, stdout") - self._emit_call_aligned("fflush") elif isinstance(val_type, SetType): a = self._loc(inst.operands[0], alloc) if a != "rdi": self._emit(f" mov rdi, {a}") self._emit_call_aligned("runtime_set_to_str") self._emit(f" mov rdi, rax") + self._emit(" mov rsi, 1") self._emit_call_aligned("fputs") - self._emit(" mov rdi, stdout") - self._emit_call_aligned("fflush") else: a = self._loc(inst.operands[0], alloc) fmt = self._get_format_label("%ld") self._emit(f" lea rdi, [rip + {fmt}]") self._emit(f" mov rsi, {a}") + self._emit(" xor eax, eax") self._emit_call_aligned("printf") return @@ -919,13 +1653,13 @@ def _emit_instruction( self._emit_call_aligned("puts") elif val_type == IRType.FLOAT64: a = self._loc(inst.operands[0], alloc) - fmt = self._get_format_label("%g") + fmt = self._get_format_label("%g\n") self._emit(f" lea rdi, [rip + {fmt}]") if a.startswith("xmm"): - self._emit(f" movsd xmm1, {a}") + self._emit(f" movsd xmm0, {a}") else: - self._emit(f" movq xmm1, {a}") - self._emit(" mov rsi, rdi") + self._emit(f" movq xmm0, {a}") + self._emit(" mov eax, 1") self._emit_call_aligned("printf") elif val_type == IRType.BOOL: a = self._loc(inst.operands[0], alloc) @@ -953,6 +1687,7 @@ def _emit_instruction( fmt = self._get_format_label("%ld\n") self._emit(f" lea rdi, [rip + {fmt}]") self._emit(f" mov rsi, {a}") + self._emit(" xor eax, eax") self._emit_call_aligned("printf") return @@ -1027,15 +1762,36 @@ def _emit_instruction( if op == Op.LOAD_GLOBAL: dst = self._loc(inst.result, alloc) name = inst.imm_str or "unknown" - self._emit(f" mov {dst}, [rip + _fr_global_{name}]") - self.module.global_vars[name] = IRType.INT64 + typ = self.module.global_vars.get(name, IRType.INT64) + if dst.startswith("xmm") or typ == IRType.FLOAT64: + xdst = dst if dst.startswith("xmm") else "xmm15" + self._emit(f" movsd {xdst}, [rip + _fr_global_{name}]") + if xdst != dst: + self._emit(f" movsd {dst}, {xdst}") + else: + self._emit_int_move(f"[rip + _fr_global_{name}]", dst) + self.module.global_vars.setdefault(name, typ) return if op == Op.STORE_GLOBAL: val = self._loc(inst.operands[0], alloc) if inst.operands else "0" name = inst.imm_str or "unknown" - self._emit(f" mov [rip + _fr_global_{name}], {val}") - self.module.global_vars[name] = IRType.INT64 + val_type = inst.operands[0].type if inst.operands else IRType.INT64 + managed = self._managed_type(val_type) + if managed: + self._emit(f" mov rsi, {val}") + self._emit(f" lea rdi, [rip + _fr_global_{name}]") + self._emit(" mov rdx, 1") + self._emit_call_aligned("runtime_store_global") + self.module.global_vars[name] = val_type + elif val.startswith("xmm"): + self._emit(f" movsd [rip + _fr_global_{name}], {val}") + else: + if val.startswith("["): + self._emit(f" mov r10, {val}") + val = "r10" + self._emit(f" mov [rip + _fr_global_{name}], {val}") + self.module.global_vars.setdefault(name, val_type) return # ── SELECT (branchless conditional) ── @@ -1079,12 +1835,43 @@ def _emit_binop(self, inst: Instruction, alloc: RegAllocation, mnemonic: str): if dst == a: self._emit(f" {mnemonic} {dst}, {b}") - elif dst == b and mnemonic in ("add", "and", "or", "xor"): + return + if dst == b and mnemonic in ("add", "and", "or", "xor"): # Commutative — swap self._emit(f" {mnemonic} {dst}, {a}") - else: - self._emit(f" mov {dst}, {a}") - self._emit(f" {mnemonic} {dst}, {b}") + return + + if mnemonic in ("add", "sub"): + from optimizer.passes import _get_const_value + + ca = _get_const_value(inst.operands[0]) + cb = _get_const_value(inst.operands[1]) + if ( + not a.startswith("[") + and not dst.startswith("[") + and not dst.startswith("xmm") + ): + if cb is not None and isinstance(cb, int): + delta = cb if mnemonic == "add" else -cb + if -2**31 <= delta < 2**31: + self._emit(f" lea {dst}, [{a} + {delta}]") + return + if mnemonic == "add" and ca is not None and isinstance(ca, int): + if not b.startswith("["): + self._emit(f" lea {dst}, [{b} + {ca}]") + return + if mnemonic == "add" and not b.startswith("["): + self._emit(f" lea {dst}, [{a} + {b}]") + return + + if dst.startswith("[") and a.startswith("["): + self._emit(f" mov r10, {a}") + a = "r10" + if dst.startswith("[") and b.startswith("["): + self._emit(f" mov r11, {b}") + b = "r11" + self._emit_int_move(a, dst) + self._emit(f" {mnemonic} {dst}, {b}") def _emit_float_binop(self, inst: Instruction, alloc: RegAllocation, mnemonic: str): """Emit a binary float operation.""" @@ -1153,7 +1940,11 @@ def _emit_compare(self, inst: Instruction, alloc: RegAllocation): self._emit(f" movq {b_xmm}, {b}") self._emit(f" ucomisd {a_xmm}, {b_xmm}") else: - self._emit(f" cmp {a}, {b}") + if a.startswith("[") and b.startswith("["): + self._emit(f" mov rax, {a}") + self._emit(f" cmp rax, {b}") + else: + self._emit(f" cmp {a}, {b}") cc_map = { Op.LT: "setl", @@ -1175,41 +1966,79 @@ def _emit_compare(self, inst: Instruction, alloc: RegAllocation): } cc = cc_map.get(inst.op, "sete") - dst_byte = _byte(dst) - self._emit(f" {cc} {dst_byte}") - self._emit(f" movzx {dst}, {dst_byte}") + if dst.startswith("["): + self._emit(f" {cc} al") + self._emit(" movzx rax, al") + self._emit(f" mov {dst}, rax") + else: + dst_byte = _byte(dst) + self._emit(f" {cc} {dst_byte}") + self._emit(f" movzx {dst}, {dst_byte}") def _emit_struct_alloc(self, inst: Instruction, alloc: RegAllocation): - """Emit struct allocation using bump allocator.""" + """Allocate a struct through the runtime allocator. + + Field values are staged on the stack before the allocation call so + they cannot be clobbered by caller-saved registers. The runtime copies + them into the new allocation and returns a direct pointer. + """ n_fields = len(inst.operands) size = n_fields * 8 if size == 0: - size = 8 # Minimum allocation + size = 8 dst = self._loc(inst.result, alloc) - # Bump allocate: ptr = heap_ptr; heap_ptr += size - self._emit(f" mov {dst}, [rip + _fr_heap_ptr]") - self._emit(f" add qword ptr [rip + _fr_heap_ptr], {size}") - - # Store initial field values - for i, field_val in enumerate(inst.operands): - val_loc = self._loc(field_val, alloc) - offset = i * 8 - if val_loc.startswith("xmm"): - self._emit(f" movsd [{dst} + {offset}], {val_loc}") - elif dst.startswith("["): - self._emit(f" mov r11, {dst}") - self._emit(f" mov [r11 + {offset}], {val_loc}") - else: - self._emit(f" mov [{dst} + {offset}], {val_loc}") + stack_offset = getattr(self, "_stack_alloc_offsets", {}).get(id(inst)) + if stack_offset is not None: + for i, field_val in enumerate(inst.operands): + val_loc = self._loc(field_val, alloc) + offset = i * 8 + if val_loc.startswith("xmm"): + self._emit(f" movsd [rbp - {stack_offset - offset}], {val_loc}") + else: + self._emit_int_move( + val_loc, f"[rbp - {stack_offset - offset}]" + ) + self._emit_lea(dst, f"[rbp - {stack_offset}]") + return + + array_size = n_fields * 8 + aligned_size = (array_size + 15) & ~15 + if aligned_size: + self._emit(f" sub rsp, {aligned_size}") + for i, field_val in enumerate(inst.operands): + val_loc = self._loc(field_val, alloc) + offset = i * 8 + if val_loc.startswith("xmm"): + self._emit(f" movsd [rsp + {offset}], {val_loc}") + elif val_loc.startswith("["): + self._emit(f" mov r11, {val_loc}") + self._emit(f" mov [rsp + {offset}], r11") + else: + self._emit(f" mov [rsp + {offset}], {val_loc}") + + self._emit(f" mov rdi, {size}") + self._emit(f" mov rsi, {n_fields}") + self._emit(f" mov rdx, rsp") + self._emit(f" mov rcx, {self._struct_managed_mask(inst.result.type)}") + self._emit_call_aligned("runtime_struct_new") + if aligned_size: + self._emit(f" add rsp, {aligned_size}") + if dst != "rax": + self._emit(f" mov {dst}, rax") def _emit_call(self, inst: Instruction, alloc: RegAllocation): """Emit a function call using System V AMD64 ABI.""" func_name = self._asm_func_name(inst.imm_str) args = inst.operands - # Set up arguments in ABI registers + # Set up arguments in ABI registers. Use parallel move resolution: + # setting rdi before moving the old rdi to rsi would otherwise clobber + # an argument (common with host/port/path style calls). + int_moves: list[tuple[str, str]] = [] + float_moves: list[tuple[str, str]] = [] + float_from_gpr: list[tuple[str, str]] = [] int_idx = 0 float_idx = 0 for arg in args: @@ -1220,20 +2049,23 @@ def _emit_call(self, inst: Instruction, alloc: RegAllocation): if float_idx < len(FLOAT_ARG_REGS) else None ) - if target: + if target and arg_loc != target: if arg_loc.startswith("xmm"): - if arg_loc != target: - self._emit(f" movsd {target}, {arg_loc}") + float_moves.append((arg_loc, target)) else: - self._emit(f" movq {target}, {arg_loc}") + float_from_gpr.append((arg_loc, target)) float_idx += 1 else: target = ARG_REGS[int_idx] if int_idx < len(ARG_REGS) else None - if target: - if arg_loc != target: - self._emit(f" mov {target}, {arg_loc}") + if target and arg_loc != target: + int_moves.append((arg_loc, target)) int_idx += 1 + self._emit_parallel_int_moves(int_moves) + self._emit_parallel_float_moves(float_moves) + for src, target in float_from_gpr: + self._emit(f" movq {target}, {src}") + self._emit_call_aligned(func_name) # Move return value to destination @@ -1299,37 +2131,18 @@ def _emit_builtin_call(self, inst: Instruction, alloc: RegAllocation): rt_name = runtime_map.get(name, f"runtime_{name}") n_args = len(inst.operands) - - # Set up args — handle float/int register type mismatches - for i, arg in enumerate(inst.operands): - arg_loc = self._loc(arg, alloc) - if i < len(ARG_REGS): - target_reg = ARG_REGS[i] - if arg_loc != target_reg: - if arg_loc.startswith("xmm"): - self._emit(f" movq {target_reg}, {arg_loc}") - elif target_reg.startswith("xmm"): - self._emit(f" movq {target_reg}, {arg_loc}") - else: - self._emit(f" mov {target_reg}, {arg_loc}") - - self._emit_call_aligned(rt_name) - - if inst.result: - dst = self._loc(inst.result, alloc) - ret_type = inst.result.type - if ret_type == IRType.FLOAT64: - # Float return value comes in xmm0 - if dst != "xmm0": - if dst.startswith("xmm"): - self._emit(f" movsd {dst}, xmm0") - else: - self._emit(f" movq {dst}, xmm0") - elif dst != "rax": - if dst.startswith("xmm"): - self._emit(f" movq {dst}, rax") - else: - self._emit(f" mov {dst}, rax") + float_ret = inst.result is not None and inst.result.type == IRType.FLOAT64 + float_args = [ + i for i, arg in enumerate(inst.operands) if arg.type == IRType.FLOAT64 + ] + self._emit_runtime_call( + rt_name, + inst, + alloc, + n_args, + float_args=float_args, + float_ret=float_ret, + ) def _emit_runtime_call( self, @@ -1393,6 +2206,35 @@ def _emit_runtime_call( else: self._emit(f" mov {dst}, rax") + def _emit_lea(self, dst: str, expr: str): + if dst.startswith("["): + self._emit(f" lea r11, {expr}") + self._emit(f" mov {dst}, r11") + else: + self._emit(f" lea {dst}, {expr}") + + def _emit_int_move(self, src: str, dst: str): + if src == dst: + return + if src.startswith("xmm"): + self._emit(f" movq {dst}, {src}") + elif src.startswith("[") and dst.startswith("["): + self._emit(f" mov r11, {src}") + self._emit(f" mov {dst}, r11") + else: + self._emit(f" mov {dst}, {src}") + + def _emit_float_move(self, src: str, dst: str): + if src == dst: + return + if src.startswith("[") and dst.startswith("["): + self._emit(f" movsd xmm15, {src}") + self._emit(f" movsd {dst}, xmm15") + elif src.startswith("xmm"): + self._emit(f" movsd {dst}, {src}") + else: + self._emit(f" movq {dst}, {src}") + def _emit_parallel_int_moves(self, moves: list[tuple[str, str]]): """Emit register moves handling conflicts where a target is another move's source.""" remaining = [(s, d) for s, d in moves if s != d] @@ -1404,7 +2246,7 @@ def _emit_parallel_int_moves(self, moves: list[tuple[str, str]]): src2 == dst for j, (src2, _) in enumerate(remaining) if j != i ) if not conflict: - self._emit(f" mov {dst}, {src}") + self._emit_int_move(src, dst) remaining.pop(i) progress = True break @@ -1424,7 +2266,7 @@ def _emit_parallel_float_moves(self, moves: list[tuple[str, str]]): src2 == dst for j, (src2, _) in enumerate(remaining) if j != i ) if not conflict: - self._emit(f" movsd {dst}, {src}") + self._emit_float_move(src, dst) remaining.pop(i) progress = True break @@ -1436,10 +2278,24 @@ def _emit_parallel_float_moves(self, moves: list[tuple[str, str]]): def _emit_epilogue(self, func: Function, alloc: RegAllocation): callee_save_size = len(alloc.used_callee_saved) * 8 - has_calls = self._func_has_calls(func) - has_spills = alloc.frame_size > 0 + needs_scope = self._func_needs_scope(func) + has_calls = self._func_has_calls(func) or needs_scope + has_spills = alloc.frame_size > 0 or bool( + getattr(self, "_stack_alloc_offsets", {}) + ) has_callee_saves = callee_save_size > 0 + if needs_scope: + # Drop this frame's allocation scope before tearing down the frame. + # Preserve both ABI return registers across the runtime call. + self._emit(" sub rsp, 16") + self._emit(" mov [rsp], rax") + self._emit(" movsd [rsp + 8], xmm0") + self._emit_call_aligned("runtime_scope_pop") + self._emit(" mov rax, [rsp]") + self._emit(" movsd xmm0, [rsp + 8]") + self._emit(" add rsp, 16") + if has_spills: save_offset = alloc.frame_size for reg in alloc.used_callee_saved: @@ -1450,7 +2306,7 @@ def _emit_epilogue(self, func: Function, alloc: RegAllocation): self._emit(" pop rbp") self._emit(" ret") elif has_callee_saves: - if has_calls: + if has_calls and (callee_save_size % 16) == 0: self._emit(" add rsp, 8") for reg in reversed(alloc.used_callee_saved): self._emit(f" pop {reg}") @@ -1461,6 +2317,24 @@ def _emit_epilogue(self, func: Function, alloc: RegAllocation): else: self._emit(" ret") + def _func_needs_scope(self, func: Function) -> bool: + """True only when this function creates manage objects itself.""" + managed_builtins = { + "set_new", + "dict_new", + "str_split", + "listdir", + } + for block in func.blocks: + for inst in block.instructions: + if inst.op == Op.ALLOC_LIST or inst.op == Op.ALLOCA: + return True + if inst.op == Op.ALLOC_STRUCT and not getattr(inst, "stack_alloc", False): + return True + if inst.op == Op.CALL_BUILTIN and (inst.imm_str or "") in managed_builtins: + return True + return False + def _func_has_calls(self, func: Function) -> bool: for block in func.blocks: for inst in block.instructions: @@ -1498,68 +2372,36 @@ def _emit_rodata(self): self._emit(f"{label}:") self._emit(f' .asciz "{escaped}"') + for func in self.module.functions: + table = func.lookup_table + if not table: + continue + label = self._lookup_label(func) + self._emit(f"{label}:") + if table.get("kind") == "dense": + for value in table.get("values", []): + self._emit(f" .quad {int(value)}") + else: + for arg, value in table.get("pairs", []): + self._emit(f" .quad {int(arg)}") + self._emit(f" .quad {int(value)}") + def _emit_bss(self): - """Emit BSS section (heap, globals).""" + """Emit BSS globals. Managed allocation lives in the runtime heap.""" + if not self.module.global_vars: + return self._emit("") self._emit(".section .bss") - - if self.module.global_vars: - for name in self.module.global_vars: - self._emit(f"_fr_global_{name}:") - self._emit(f" .quad 0") - - if self._module_needs_heap(): - self._emit(".globl _fr_heap_ptr") - self._emit("_fr_heap_ptr:") + for name in self.module.global_vars: + self._emit(f".globl _fr_global_{name}") + self._emit(f"_fr_global_{name}:") self._emit(" .quad 0") - self._emit(".globl _fr_heap") - self._emit("_fr_heap:") - self._emit(" .space 16777216") - - self._emit(".globl struct_heap_base") - self._emit("struct_heap_base:") - self._emit(" .quad 0") - self._emit(".globl struct_heap_ptr") - self._emit("struct_heap_ptr:") - self._emit(" .quad 0") - - if self._module_needs_structs(): - self._emit(".globl struct_data") - self._emit("struct_data:") - self._emit(" .space 67108864") - - if self.module.global_vars: - self._emit(".globl global_vars") - self._emit("global_vars:") - self._emit(" .space 2048") + self._emit(".globl global_vars") + self._emit("global_vars:") + self._emit(" .space 2048") def _module_needs_bss(self) -> bool: - return True - - def _module_needs_heap(self) -> bool: - for func in self.module.functions: - for block in func.blocks: - for inst in block.instructions: - if inst.op in { - Op.ALLOC_LIST, - Op.LIST_APPEND, - Op.STR_CONCAT, - Op.TO_STR, - Op.TO_BOOL, - Op.SQRT, - Op.SIN, - Op.COS, - Op.TAN, - Op.ABS, - Op.FLOOR, - Op.CEIL, - Op.ROUND, - Op.POW, - Op.MIN, - Op.MAX, - }: - return True - return False + return bool(self.module.global_vars) def _module_needs_structs(self) -> bool: for func in self.module.functions: @@ -1616,7 +2458,7 @@ def _byte(reg: str) -> str: return byte_map.get(reg, "al") -def generate_asm(module: Module, opt_level: int = 2) -> str: +def generate_asm(module: Module, opt_level: int = 2, target: str = "linux") -> str: """Generate x86_64 assembly from an IR module.""" - gen = CodeGen(module, opt_level) + gen = CodeGen(module, opt_level, target) return gen.emit() diff --git a/src/optimizer/config.py b/src/optimizer/config.py new file mode 100644 index 0000000..6244e39 --- /dev/null +++ b/src/optimizer/config.py @@ -0,0 +1,79 @@ +"""Tunable budgets for the optimizer. + +Every speculative or potentially expensive analysis uses one of these limits. +Values can be overridden with environment variables so users can trade compile +time for code quality without rebuilding the compiler. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass, replace + + +def _env_int(name: str, default: int) -> int: + raw = os.environ.get(name) + if raw is None: + return default + try: + return int(raw, 0) + except ValueError: + return default + + +@dataclass(frozen=True) +class OptimizerConfig: + # Fixed-point and inlining budgets + max_iterations: int = 20 + inline_max_inst: int = 20 + specialized_inline_max_inst: int = 60 + max_specializations: int = 64 + max_specialized_body: int = 120 + max_specialized_range: int = 64 + max_specialization_depth: int = 3 + + # Interprocedural range analysis + max_range_iterations: int = 6 + max_range_visits: int = 20000 + max_range_values: int = 4096 + range_widen_after: int = 6 + + # Compile-time simulation / lookup tables + max_sim_steps: int = 200000 + max_sim_calls: int = 2000 + max_sim_time_ms: int = 100 + max_table_entries: int = 64 + max_table_domain: int = 256 + min_table_entries: int = 2 + max_table_function_size: int = 120 + + @classmethod + def from_env(cls) -> "OptimizerConfig": + return cls( + max_iterations=_env_int("FR_OPT_ITERATIONS", cls.max_iterations), + inline_max_inst=_env_int("FR_OPT_INLINE_MAX", cls.inline_max_inst), + max_specializations=_env_int( + "FR_OPT_MAX_SPECIALIZATIONS", cls.max_specializations + ), + max_specialized_body=_env_int( + "FR_OPT_MAX_SPECIALIZED_BODY", cls.max_specialized_body + ), + max_specialized_range=_env_int( + "FR_OPT_MAX_SPECIALIZED_RANGE", cls.max_specialized_range + ), + max_range_iterations=_env_int( + "FR_OPT_RANGE_ITERATIONS", cls.max_range_iterations + ), + max_range_visits=_env_int("FR_OPT_RANGE_VISITS", cls.max_range_visits), + max_sim_steps=_env_int("FR_OPT_SIM_STEPS", cls.max_sim_steps), + max_sim_calls=_env_int("FR_OPT_SIM_CALLS", cls.max_sim_calls), + max_sim_time_ms=_env_int("FR_OPT_SIM_MS", cls.max_sim_time_ms), + max_table_entries=_env_int( + "FR_OPT_TABLE_ENTRIES", cls.max_table_entries + ), + max_table_domain=_env_int("FR_OPT_TABLE_DOMAIN", cls.max_table_domain), + min_table_entries=_env_int("FR_OPT_TABLE_MIN", cls.min_table_entries), + ) + + def with_overrides(self, **kwargs) -> "OptimizerConfig": + return replace(self, **kwargs) diff --git a/src/optimizer/ir.py b/src/optimizer/ir.py index 1c9af6f..20f80f6 100644 --- a/src/optimizer/ir.py +++ b/src/optimizer/ir.py @@ -60,8 +60,24 @@ def __eq__(self, other): return isinstance(other, SetType) and self.elem_type == other.elem_type +@dataclass +class PtrType: + """A pointer to a value. Pointers are raw addresses in the native backend.""" + + pointee: "ValueType" + + def __hash__(self): + return hash(("ptr", self.pointee)) + + def __eq__(self, other): + return isinstance(other, PtrType) and self.pointee == other.pointee + + def __repr__(self): + return f"*{self.pointee}" + + # A ValueType is the type of any SSA value -ValueType = IRType | StructType | ListType | SetType +ValueType = IRType | StructType | ListType | SetType | PtrType class Op(Enum): @@ -111,6 +127,14 @@ class Op(Enum): LOAD_FIELD = auto() # load field from struct pointer STORE_FIELD = auto() # store field to struct pointer (void) + # Pointer operations (language-level pointers) + ALLOCA = auto() # allocate an addressable cell, returns pointer + ADDR_GLOBAL = auto() # address of a global variable + PTR_LOAD = auto() # load through pointer + PTR_STORE = auto() # store through pointer (void) + FIELD_ADDR = auto() # pointer to struct field (ptr, field index -> pointer) + LIST_ELEM_ADDR = auto() # pointer to list element (list, index -> pointer) + # List ALLOC_LIST = auto() LIST_GET = auto() @@ -182,11 +206,12 @@ class Op(Enum): Op.STORE_FIELD, Op.LIST_SET, Op.LIST_APPEND, Op.CALL, Op.CALL_EXTERN, Op.CALL_BUILTIN, Op.BRANCH, Op.JUMP, Op.RETURN, Op.RETURN_VOID, - Op.STORE_GLOBAL, Op.PRINT, Op.PRINTLN, Op.INPUT, + Op.STORE_GLOBAL, Op.LOAD_GLOBAL, Op.PRINT, Op.PRINTLN, Op.INPUT, Op.TRY_BEGIN, Op.TRY_END, Op.RAISE, Op.DIV, Op.MOD, Op.FDIV, Op.ALLOC_LIST, Op.LIST_GET, Op.LIST_LEN, Op.ALLOC_STRUCT, Op.LOAD_FIELD, Op.STORE_FIELD, + Op.ALLOCA, Op.ADDR_GLOBAL, Op.PTR_LOAD, Op.PTR_STORE, Op.LIST_ELEM_ADDR, Op.STR_CONCAT, Op.TO_STR, Op.TO_BOOL, }) @@ -208,7 +233,7 @@ class Op(Enum): class Value: """An SSA value. Defined by exactly one instruction.""" - __slots__ = ('id', 'type', 'defining_inst', 'uses') + __slots__ = ('id', 'type', 'defining_inst', 'uses', 'value_range') _next_id = 0 @@ -218,6 +243,7 @@ def __init__(self, typ: ValueType, defining_inst: Optional[Instruction] = None): self.type = typ self.defining_inst = defining_inst self.uses: list[Instruction] = [] # instructions that use this value + self.value_range: Optional[tuple[int, int]] = None def __repr__(self): return f'%{self.id}' @@ -266,7 +292,9 @@ class Instruction: """A single SSA instruction. Produces at most one Value.""" __slots__ = ('op', 'result', 'operands', 'block', 'source_line', - 'imm_int', 'imm_str', 'imm_float', 'target_blocks') + 'imm_int', 'imm_str', 'imm_float', 'target_blocks', + 'stack_alloc', 'pure_call', 'arg_ranges', 'no_zero_check', + 'elided', 'fused_compare', 'fused_operands') def __init__(self, op: Op, operands: list[Value] = None, result_type: ValueType = None, source_line: int = None): @@ -280,6 +308,13 @@ def __init__(self, op: Op, operands: list[Value] = None, self.imm_str: Optional[str] = None self.imm_float: Optional[float] = None self.target_blocks: list[BasicBlock] = [] # for BRANCH/JUMP + self.stack_alloc = False # ALLOC_STRUCT can live in the stack frame + self.pure_call = False # CALL/CALL_BUILTIN proven side-effect free + self.arg_ranges = None # per-argument ranges inferred at this call + self.no_zero_check = False # divisor proven non-zero + self.elided = False # skipped by codegen (folded into branch) + self.fused_compare = None # comparison folded into BRANCH + self.fused_operands = None # Create result value if the instruction produces one if result_type is not None and result_type != IRType.VOID: @@ -312,6 +347,8 @@ def is_terminator(self) -> bool: @property def has_side_effects(self) -> bool: + if self.op in (Op.CALL, Op.CALL_BUILTIN) and self.pure_call: + return False return self.op in SIDE_EFFECT_OPS def __repr__(self): @@ -377,7 +414,8 @@ class Function: """An SSA function with basic blocks.""" __slots__ = ('name', 'params', 'return_type', 'blocks', 'entry', - 'is_extern', 'source_file', 'local_names') + 'is_extern', 'source_file', 'local_names', 'param_ranges', + 'lookup_table') def __init__(self, name: str, params: list[Param] = None, return_type: ValueType = IRType.VOID): @@ -389,6 +427,8 @@ def __init__(self, name: str, params: list[Param] = None, self.is_extern = False self.source_file: Optional[str] = None self.local_names: dict[int, str] = {} # maps var index → name for debugging + self.param_ranges: dict[int, tuple[int, int]] = {} + self.lookup_table = None def add_block(self, block: BasicBlock) -> BasicBlock: block.func = self diff --git a/src/optimizer/ir_builder.py b/src/optimizer/ir_builder.py index 7eee9b9..735ecdb 100644 --- a/src/optimizer/ir_builder.py +++ b/src/optimizer/ir_builder.py @@ -20,6 +20,7 @@ StructType, ListType, SetType, + PtrType, ValueType, ) @@ -48,6 +49,23 @@ def _parse_type(type_str: str, module: Module) -> ValueType: return _TYPE_MAP.get(type_str, IRType.INT64) +def _unescape_bytecode_string(s: str) -> str: + """Decode the C-style escapes used by the bytecode writer.""" + out = [] + i = 0 + while i < len(s): + ch = s[i] + if ch != "\\" or i + 1 >= len(s): + out.append(ch) + i += 1 + continue + nxt = s[i + 1] + mapping = {"n": "\n", "r": "\r", "t": "\t", "0": "\0"} + out.append(mapping.get(nxt, nxt)) + i += 2 + return "".join(out) + + def _parse_line(line: str) -> tuple[str, list[str]]: """Parse a bytecode line into (opcode, args).""" line = line.strip() @@ -131,6 +149,11 @@ def _parse_metadata(self, lines: list[str]): self._struct_id_map[sid].name = name self.module.add_struct(self._struct_id_map[sid]) + elif op == ".global" and args: + name = args[0] + typ = _parse_type(args[1], self.module) if len(args) > 1 else IRType.INT64 + self.module.global_vars[name] = typ + elif op == ".entry" and args: self.module.entry_func = args[0].rstrip("%") @@ -298,6 +321,7 @@ def _simulate_and_build_ir(self, func: Function, raw_blocks: list): # Variable versions: track current SSA value for each local variable index # This is pre-SSA: we'll insert phi nodes later var_versions: dict[int, Value] = {} + self._boxed_locals: dict[int, Value] = {} # Initialize params for p in func.params: @@ -356,6 +380,53 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): block.append(inst) return inst + def _make_entry_inst(ir_op, operands, result_type, **kwargs): + entry = func.entry + inst = Instruction(ir_op, operands, result_type, source_line) + for k, v in kwargs.items(): + setattr(inst, k, v) + inst.block = entry + if entry.terminator is not None: + entry.instructions.insert(len(entry.instructions) - 1, inst) + else: + entry.instructions.append(inst) + return inst + + def _var_value(idx: int) -> Value: + if idx in self._boxed_locals: + ptr = self._boxed_locals[idx] + pointee = ptr.type.pointee if isinstance(ptr.type, PtrType) else IRType.INT64 + inst = _make_inst(Op.PTR_LOAD, [ptr], pointee) + return inst.result or ptr + val = var_versions.get(idx) + if val is None: + typ = self._local_types.get(func.name, {}).get(idx, IRType.INT64) + val = self._make_const(typ, 0, block, source_line) + var_versions[idx] = val + return val + + def _set_var(idx: int, val: Value) -> None: + if idx in self._boxed_locals: + _make_inst(Op.PTR_STORE, [self._boxed_locals[idx], val], None) + else: + var_versions[idx] = val + + def _box_var(idx: int) -> Value: + if idx in self._boxed_locals: + return self._boxed_locals[idx] + current = _var_value(idx) + pointee = current.type if current is not None else IRType.INT64 + # Allocate in the entry block: a box created under a branch must + # dominate every later pointer load. + inst = _make_entry_inst(Op.ALLOCA, [], PtrType(pointee)) + ptr = inst.result + if ptr is None: + ptr = self._make_const(PtrType(pointee), 0, block, source_line) + _make_inst(Op.PTR_STORE, [ptr, current], None) + self._boxed_locals[idx] = ptr + var_versions.pop(idx, None) + return ptr + # ── Constants ── if op == "CONST_I64": @@ -372,7 +443,9 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): if op == "CONST_STR": for a in (args if args else [""]): - v = self._make_const(IRType.STRING, a, block, source_line) + v = self._make_const( + IRType.STRING, _unescape_bytecode_string(a), block, source_line + ) _push(v) return @@ -386,20 +459,12 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): if op == "LOAD": for a in args: - idx = int(a) - val = var_versions.get(idx) - if val is None: - typ = self._local_types.get(func.name, {}).get(idx, IRType.INT64) - val = self._make_const(typ, 0, block, source_line) - var_versions[idx] = val - _push(val) + _push(_var_value(int(a))) return if op == "STORE": for a in reversed(args): - idx = int(a) - val = _pop() - var_versions[idx] = val + _set_var(int(a), _pop()) return if op == "STORE_CONST_I64": @@ -408,7 +473,7 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): val = self._make_const( IRType.INT64, int(args[i + 1]), block, source_line ) - var_versions[idx] = val + _set_var(idx, val) return if op == "STORE_CONST_F64": @@ -417,7 +482,7 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): val = self._make_const( IRType.FLOAT64, float(args[i + 1]), block, source_line ) - var_versions[idx] = val + _set_var(idx, val) return if op == "STORE_CONST_BOOL": @@ -429,14 +494,69 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): block, source_line, ) - var_versions[idx] = val + _set_var(idx, val) return if op == "STORE_CONST_STR": for i in range(0, len(args) - 1, 2): idx = int(args[i]) - val = self._make_const(IRType.STRING, args[i + 1], block, source_line) - var_versions[idx] = val + val = self._make_const( + IRType.STRING, + _unescape_bytecode_string(args[i + 1]), + block, + source_line, + ) + _set_var(idx, val) + return + + if op == "ADDR_GLOBAL" and args: + gtype = self.module.global_vars.get(args[0], IRType.INT64) + inst = _make_inst( + Op.ADDR_GLOBAL, [], PtrType(gtype), imm_str=args[0] + ) + if inst.result: + _push(inst.result) + return + + if op == "ADDR_LOCAL" and args: + ptr = _box_var(int(args[0])) + _push(ptr) + return + + if op == "ADDR_FIELD" and args: + field_idx = int(args[0]) + struct_val = _pop() + pointee = IRType.INT64 + if isinstance(struct_val.type, StructType) and field_idx < len(struct_val.type.field_types): + pointee = struct_val.type.field_types[field_idx] + inst = _make_inst(Op.FIELD_ADDR, [struct_val], PtrType(pointee), imm_int=field_idx) + if inst.result: + _push(inst.result) + return + + if op == "ADDR_ELEM": + index = _pop() + list_val = _pop() + pointee = IRType.INT64 + if isinstance(list_val.type, ListType): + pointee = list_val.type.elem_type + inst = _make_inst(Op.LIST_ELEM_ADDR, [list_val, index], PtrType(pointee)) + if inst.result: + _push(inst.result) + return + + if op == "DEREF": + ptr = _pop() + pointee = ptr.type.pointee if isinstance(ptr.type, PtrType) else IRType.INT64 + inst = _make_inst(Op.PTR_LOAD, [ptr], pointee) + if inst.result: + _push(inst.result) + return + + if op == "DEREF_SET": + value = _pop() + ptr = _pop() + _make_inst(Op.PTR_STORE, [ptr, value], None) return if op == "FUSED_LOAD_STORE": @@ -447,18 +567,9 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): while i < len(args): idx = int(args[i]) if expect_load: - # LOAD: push var onto stack - val = var_versions.get(idx) - if val is None: - typ = self._local_types.get(func.name, {}).get( - idx, IRType.INT64 - ) - val = self._make_const(typ, 0, block, source_line) - _push(val) + _push(_var_value(idx)) else: - # STORE: pop from stack into var - val = _pop() - var_versions[idx] = val + _set_var(idx, _pop()) expect_load = not expect_load i += 1 return @@ -468,11 +579,8 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): store_idx = int(args[i]) load_idx = int(args[i + 1]) val = _pop() - var_versions[store_idx] = val - load_val = var_versions.get(load_idx) - if load_val is None: - load_val = self._make_const(IRType.INT64, 0, block, source_line) - _push(load_val) + _set_var(store_idx, val) + _push(_var_value(load_idx)) return if op == "FUSED_GET_STORE_LOAD": @@ -485,43 +593,31 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): Op.LOAD_FIELD, [struct_val], IRType.INT64, imm_int=field_idx ) if inst.result: - var_versions[store_idx] = inst.result - load_val = var_versions.get(load_idx) - if load_val is None: - load_val = self._make_const(IRType.INT64, 0, block, source_line) - _push(load_val) + _set_var(store_idx, inst.result) + _push(_var_value(load_idx)) return if op in ("COPY_LOCAL", "COPY_LOCAL_REF"): if len(args) >= 2: src = int(args[0]) dst = int(args[1]) - val = var_versions.get(src) - if val is None: - val = self._make_const(IRType.INT64, 0, block, source_line) - var_versions[dst] = val + _set_var(dst, _var_value(src)) return if op == "INC_LOCAL" and args: idx = int(args[0]) - val = var_versions.get(idx) - if val is None: - val = self._make_const(IRType.INT64, 0, block, source_line) one = self._make_const(IRType.INT64, 1, block, source_line) - inst = _make_inst(Op.ADD, [val, one], IRType.INT64) + inst = _make_inst(Op.ADD, [_var_value(idx), one], IRType.INT64) if inst.result: - var_versions[idx] = inst.result + _set_var(idx, inst.result) return if op == "DEC_LOCAL" and args: idx = int(args[0]) - val = var_versions.get(idx) - if val is None: - val = self._make_const(IRType.INT64, 0, block, source_line) one = self._make_const(IRType.INT64, 1, block, source_line) - inst = _make_inst(Op.SUB, [val, one], IRType.INT64) + inst = _make_inst(Op.SUB, [_var_value(idx), one], IRType.INT64) if inst.result: - var_versions[idx] = inst.result + _set_var(idx, inst.result) return # ── Stack manipulation ── @@ -582,7 +678,11 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): if op == "NEG": a = _pop() - inst = _make_inst(Op.NEG, [a], IRType.INT64) + inst = _make_inst( + Op.NEG, + [a], + IRType.FLOAT64 if a.type == IRType.FLOAT64 else IRType.INT64, + ) if inst.result: _push(inst.result) return @@ -681,6 +781,31 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): # ── LOAD2 fused ops ── + _load2_ops = { + "LOAD2_ADD_I64": (Op.ADD, IRType.INT64), + "LOAD2_SUB_I64": (Op.SUB, IRType.INT64), + "LOAD2_MUL_I64": (Op.MUL, IRType.INT64), + "LOAD2_DIV_I64": (Op.DIV, IRType.INT64), + "LOAD2_MOD_I64": (Op.MOD, IRType.INT64), + "LOAD2_ADD_F64": (Op.FADD, IRType.FLOAT64), + "LOAD2_SUB_F64": (Op.FSUB, IRType.FLOAT64), + "LOAD2_MUL_F64": (Op.FMUL, IRType.FLOAT64), + "LOAD2_DIV_F64": (Op.FDIV, IRType.FLOAT64), + "LOAD2_AND_I64": (Op.AND, IRType.BOOL), + "LOAD2_OR_I64": (Op.OR, IRType.BOOL), + "LOAD2_XOR_I64": (Op.BIT_XOR, IRType.INT64), + "LOAD2_SHL_I64": (Op.SHL, IRType.INT64), + "LOAD2_SHR_I64": (Op.SHR, IRType.INT64), + } + if op in _load2_ops and len(args) >= 2: + ir_op, ret_type = _load2_ops[op] + a = _var_value(int(args[0])) + b = _var_value(int(args[1])) + inst = _make_inst(ir_op, [a, b], ret_type) + if inst.result: + _push(inst.result) + return + if op == "LOAD2_ADD_I64" and len(args) >= 2: a = var_versions.get(int(args[0])) b = var_versions.get(int(args[1])) @@ -721,6 +846,20 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): "LOAD2_CMP_NE": Op.NE, } ir_op = cmp_map.get(op, Op.LT) + if op in ("LOAD2_CMP_EQ", "LOAD2_CMP_NE") and ( + a.type == IRType.STRING or b.type == IRType.STRING + ): + inst = _make_inst( + Op.CALL_BUILTIN, [a, b], IRType.BOOL, imm_str="str_eq" + ) + if inst.result: + if op == "LOAD2_CMP_NE": + not_inst = _make_inst(Op.NOT, [inst.result], IRType.BOOL) + if not_inst.result: + _push(not_inst.result) + else: + _push(inst.result) + return inst = _make_inst(ir_op, [a, b], IRType.BOOL) if inst.result: _push(inst.result) @@ -756,22 +895,46 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): _push(inst.result) return - if op == "CMP_EQ": + if op in ("CMP_EQ", "CMP_NE"): b, a = _pop(), _pop() - inst = _make_inst(Op.EQ, [a, b], IRType.BOOL) + if a.type == IRType.STRING or b.type == IRType.STRING: + inst = _make_inst( + Op.CALL_BUILTIN, [a, b], IRType.BOOL, imm_str="str_eq" + ) + if inst.result: + if op == "CMP_NE": + not_inst = _make_inst(Op.NOT, [inst.result], IRType.BOOL) + if not_inst.result: + _push(not_inst.result) + else: + _push(inst.result) + return + inst = _make_inst(Op.EQ if op == "CMP_EQ" else Op.NE, [a, b], IRType.BOOL) if inst.result: _push(inst.result) return - if op == "CMP_NE": - b, a = _pop(), _pop() - inst = _make_inst(Op.NE, [a, b], IRType.BOOL) + # ── Const-fused comparison ── + + if op in ( + "CMP_LT_CONST_F64", "CMP_GT_CONST_F64", "CMP_LE_CONST_F64", + "CMP_GE_CONST_F64", "CMP_EQ_CONST_F64", "CMP_NE_CONST_F64", + ) and args: + a = _pop() + c = self._make_const(IRType.FLOAT64, float(args[0]), block, source_line) + cmp_map = { + "CMP_LT_CONST_F64": Op.LT, + "CMP_GT_CONST_F64": Op.GT, + "CMP_LE_CONST_F64": Op.LE, + "CMP_GE_CONST_F64": Op.GE, + "CMP_EQ_CONST_F64": Op.EQ, + "CMP_NE_CONST_F64": Op.NE, + } + inst = _make_inst(cmp_map[op], [a, c], IRType.BOOL) if inst.result: _push(inst.result) return - # ── Const-fused comparison ── - if op == "CMP_LT_CONST" and args: a = _pop() c = self._make_const(IRType.INT64, int(args[0]), block, source_line) @@ -1037,6 +1200,31 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): _push(inst.result) return + if op == "LIST_NEW_CAP" and args: + capacity = int(args[0]) + elem_code = int(args[1]) if len(args) > 1 else -1 + elem_type = { + 0: IRType.INT64, + 1: IRType.STRING, + 2: IRType.FLOAT64, + 3: IRType.BOOL, + }.get(elem_code, IRType.INT64) + zero = ( + "" + if elem_type == IRType.STRING + else (0.0 if elem_type == IRType.FLOAT64 else 0) + ) + elems = [ + self._make_const(elem_type, zero, block, source_line) + for _ in range(max(0, capacity)) + ] + inst = _make_inst( + Op.ALLOC_LIST, elems, ListType(elem_type), imm_int=capacity + ) + if inst.result: + _push(inst.result) + return + if op == "LIST_NEW_STACK" and args: count = int(args[0]) elems = [] @@ -1052,6 +1240,12 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): if op == "LIST_APPEND": val = _pop() lst = _pop() + if ( + isinstance(lst.type, ListType) + and lst.type.elem_type == IRType.INT64 + and val.type != IRType.INT64 + ): + lst.type = ListType(val.type) _make_inst(Op.LIST_APPEND, [lst, val]) _push(lst) return @@ -1059,8 +1253,14 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): if op == "LIST_GET": idx = _pop() lst = _pop() - # String indexing returns a string character - result_type = IRType.STRING if lst.type == IRType.STRING else IRType.INT64 + # String indexing returns a string character; typed lists return + # their element type so downstream calls/prints get the right ABI. + if isinstance(lst.type, ListType): + result_type = lst.type.elem_type + elif lst.type == IRType.STRING: + result_type = IRType.STRING + else: + result_type = IRType.INT64 inst = _make_inst(Op.LIST_GET, [lst, idx], result_type) if inst.result: _push(inst.result) @@ -1070,6 +1270,12 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): val = _pop() idx = _pop() lst = _pop() + if ( + isinstance(lst.type, ListType) + and lst.type.elem_type == IRType.INT64 + and val.type != IRType.INT64 + ): + lst.type = ListType(val.type) _make_inst(Op.LIST_SET, [lst, idx, val]) _push(lst) return @@ -1083,7 +1289,8 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): if op == "LIST_POP": lst = _pop() - inst = _make_inst(Op.CALL_BUILTIN, [lst], IRType.INT64, imm_str="list_pop") + result_type = lst.type.elem_type if isinstance(lst.type, ListType) else IRType.INT64 + inst = _make_inst(Op.CALL_BUILTIN, [lst], result_type, imm_str="list_pop") # LIST_POP in bytecode pushes (modified_list, popped_value) # The list is modified in-place, so push list back first, then result _push(lst) @@ -1130,8 +1337,9 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): return if op == "ASSERT": - msg = _pop() + # Bytecode stack order is: message, then condition. cond = _pop() + msg = _pop() _make_inst(Op.CALL_BUILTIN, [cond, msg], IRType.VOID, imm_str="assert") return @@ -1153,8 +1361,8 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): return if op == "CONST_BYTES": - # Treat as a string constant for now - text = args[0] if args else "" + # Bytes share the escaped string payload representation. + text = _unescape_bytecode_string(args[0]) if args else "" v = self._make_const(IRType.STRING, text, block, source_line) _push(v) return @@ -1262,14 +1470,21 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): # ── Global variables ── if op == "LOAD_GLOBAL" and args: - inst = _make_inst(Op.LOAD_GLOBAL, [], IRType.INT64, imm_str=args[0]) + gtype = self.module.global_vars.get(args[0], IRType.INT64) + inst = _make_inst(Op.LOAD_GLOBAL, [], gtype, imm_str=args[0]) if inst.result: _push(inst.result) return if op == "STORE_GLOBAL" and args: val = _pop() - _make_inst(Op.STORE_GLOBAL, [val], imm_str=args[0]) + name = args[0] + current = self.module.global_vars.get(name) + if isinstance(val.type, (StructType, ListType)) and not isinstance( + current, (StructType, ListType) + ): + self.module.global_vars[name] = val.type + _make_inst(Op.STORE_GLOBAL, [val], imm_str=name) return # ── Switch ── @@ -1366,11 +1581,22 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): if op in _math_ops: ir_op, ret = _math_ops[op] + + def _as_float(value): + if value.type == IRType.INT64: + conv = _make_inst(Op.INT_TO_FLOAT, [value], IRType.FLOAT64) + return conv.result or value + return value + if op in ("MIN", "MAX", "POW"): b, a = _pop(), _pop() + if ir_op == Op.POW: + a, b = _as_float(a), _as_float(b) inst = _make_inst(ir_op, [a, b], ret) else: a = _pop() + if ir_op in (Op.SQRT, Op.SIN, Op.COS, Op.TAN): + a = _as_float(a) inst = _make_inst(ir_op, [a], ret) if inst.result: _push(inst.result) @@ -1452,6 +1678,12 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): var_versions[idx] = inst.result return + if op == "GETPID": + inst = _make_inst(Op.CALL_BUILTIN, [], IRType.INT64, imm_str="getpid") + if inst.result: + _push(inst.result) + return + # ── File I/O, set, dict, etc. → CALL_BUILTIN ── if op.startswith("FILE_"): @@ -1461,6 +1693,22 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): "FILE_WRITE": ("fwrite", 2, IRType.INT64), "FILE_READ": ("fread", 2, IRType.STRING), "FILE_CLOSE": ("fclose", 1, IRType.VOID), + "FILE_EXISTS": ("exists", 1, IRType.BOOL), + "FILE_ISFILE": ("isfile", 1, IRType.BOOL), + "FILE_ISDIR": ("isdir", 1, IRType.BOOL), + "FILE_LISTDIR": ("listdir", 1, ListType(IRType.STRING)), + "FILE_MKDIR": ("mkdir", 1, IRType.VOID), + "FILE_MAKEDIRS": ("makedirs", 1, IRType.VOID), + "FILE_REMOVE": ("remove", 1, IRType.VOID), + "FILE_RMDIR": ("rmdir", 1, IRType.VOID), + "FILE_RENAME": ("rename", 2, IRType.VOID), + "FILE_GETSIZE": ("getsize", 1, IRType.INT64), + "FILE_GETCWD": ("getcwd", 0, IRType.STRING), + "FILE_CHDIR": ("chdir", 1, IRType.VOID), + "FILE_ABSPATH": ("abspath", 1, IRType.STRING), + "FILE_BASENAME": ("basename", 1, IRType.STRING), + "FILE_DIRNAME": ("dirname", 1, IRType.STRING), + "FILE_JOIN": ("pathjoin", 1, IRType.STRING), } if op in file_map: name, n_args, ret_type = file_map[op] @@ -1476,8 +1724,24 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): if op in ("SET_ADD", "SET_REMOVE"): val = _pop() s = _pop() - # void functions — don't use return value, re-push the set - _make_inst(Op.CALL_BUILTIN, [s, val], IRType.VOID, imm_str=builtin_name) + if op == "SET_ADD": + if isinstance(val.type, (StructType, ListType, SetType)): + elem_code = 4 + elif val.type == IRType.STRING: + elem_code = 1 + else: + elem_code = 0 + code = self._make_const(IRType.INT64, elem_code, block, source_line) + _make_inst( + Op.CALL_BUILTIN, + [s, val, code], + IRType.VOID, + imm_str="set_add_typed", + ) + else: + _make_inst( + Op.CALL_BUILTIN, [s, val], IRType.VOID, imm_str=builtin_name + ) _push(s) elif op == "SET_CONTAINS": val = _pop() @@ -1508,14 +1772,54 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): if inst.result: _push(inst.result) elif op == "DICT_SET": - builtin_args = [_pop(), _pop()] + value = _pop() + key = _pop() + d = _pop() + key_code = self._make_const( + IRType.INT64, + 1 if key.type == IRType.STRING else 0, + block, + source_line, + ) _make_inst( - Op.CALL_BUILTIN, builtin_args, IRType.VOID, imm_str=builtin_name + Op.CALL_BUILTIN, + [d, key, value, key_code], + IRType.VOID, + imm_str="dict_set_typed", + ) + # The bytecode leaves the dictionary for the following STORE. + _push(d) + elif op == "DICT_GET": + key = _pop() + d = _pop() + key_code = self._make_const( + IRType.INT64, + 1 if key.type == IRType.STRING else 0, + block, + source_line, ) - elif op in ("DICT_GET", "DICT_CONTAINS"): - builtin_args = [_pop()] inst = _make_inst( - Op.CALL_BUILTIN, builtin_args, IRType.INT64, imm_str=builtin_name + Op.CALL_BUILTIN, + [d, key, key_code], + IRType.INT64, + imm_str="dict_get_typed", + ) + if inst.result: + _push(inst.result) + elif op == "DICT_CONTAINS": + key = _pop() + d = _pop() + key_code = self._make_const( + IRType.INT64, + 1 if key.type == IRType.STRING else 0, + block, + source_line, + ) + inst = _make_inst( + Op.CALL_BUILTIN, + [d, key, key_code], + IRType.BOOL, + imm_str="dict_contains_typed", ) if inst.result: _push(inst.result) @@ -1599,6 +1903,80 @@ def _make_inst(ir_op, operands=None, result_type=None, **kwargs): _push(inst.result) return + # ── Sockets ── + if op == "SOCKET_CREATE": + typ = _pop() + family = _pop() + inst = _make_inst( + Op.CALL_BUILTIN, [family, typ], IRType.INT64, imm_str="socket_create" + ) + if inst.result: + _push(inst.result) + return + if op in ("SOCKET_CONNECT", "SOCKET_BIND"): + port = _pop() + host = _pop() + sock = _pop() + name = "socket_connect" if op == "SOCKET_CONNECT" else "socket_bind" + _make_inst(Op.CALL_BUILTIN, [sock, host, port], IRType.VOID, imm_str=name) + return + if op == "SOCKET_LISTEN": + backlog = _pop() + sock = _pop() + _make_inst( + Op.CALL_BUILTIN, [sock, backlog], IRType.VOID, imm_str="socket_listen" + ) + return + if op == "SOCKET_ACCEPT": + sock = _pop() + inst = _make_inst( + Op.CALL_BUILTIN, [sock], IRType.INT64, imm_str="socket_accept" + ) + if inst.result: + _push(inst.result) + return + if op == "SOCKET_SEND": + data = _pop() + sock = _pop() + inst = _make_inst( + Op.CALL_BUILTIN, [sock, data], IRType.INT64, imm_str="socket_send" + ) + if inst.result: + _push(inst.result) + return + if op == "SOCKET_SENDALL": + data = _pop() + sock = _pop() + _make_inst( + Op.CALL_BUILTIN, [sock, data], IRType.VOID, imm_str="socket_sendall" + ) + return + if op == "SOCKET_RECV": + size = _pop() + sock = _pop() + inst = _make_inst( + Op.CALL_BUILTIN, [sock, size], IRType.STRING, imm_str="socket_recv" + ) + if inst.result: + _push(inst.result) + return + if op == "SOCKET_CLOSE": + sock = _pop() + _make_inst(Op.CALL_BUILTIN, [sock], IRType.VOID, imm_str="socket_close") + return + if op == "SOCKET_SETSOCKOPT": + value = _pop() + option = _pop() + level = _pop() + sock = _pop() + _make_inst( + Op.CALL_BUILTIN, + [sock, level, option, value], + IRType.VOID, + imm_str="socket_setsockopt", + ) + return + # ── Extern calls (C FFI) ── # These are handled as CALL with extern flag; the native codegen # will detect c_import functions by name @@ -1662,22 +2040,33 @@ def _build_cfg_edges(self, func: Function): term.target_blocks = [target] elif term.op == Op.BRANCH: - # BRANCH cond, false_label - # True branch = fall through to next block - # False branch = jump to label - false_label = term.imm_str - if i + 1 < len(blocks): - true_block = blocks[i + 1] - block.successors.append(true_block) - true_block.predecessors.append(block) - if false_label in block_map: - false_block = block_map[false_label] - block.successors.append(false_block) - false_block.predecessors.append(block) - term.target_blocks = [ - blocks[i + 1] if i + 1 < len(blocks) else false_block, - false_block, - ] + # Explicit target_blocks come from switch lowering and are + # already authoritative (inner switch blocks have no imm_str). + if term.target_blocks: + for target in term.target_blocks: + if target not in block.successors: + block.successors.append(target) + if block not in target.predecessors: + target.predecessors.append(block) + else: + # BRANCH cond, false_label: true = fall through. + false_label = term.imm_str + if i + 1 < len(blocks): + true_block = blocks[i + 1] + if true_block not in block.successors: + block.successors.append(true_block) + if block not in true_block.predecessors: + true_block.predecessors.append(block) + if false_label in block_map: + false_block = block_map[false_label] + if false_block not in block.successors: + block.successors.append(false_block) + if block not in false_block.predecessors: + false_block.predecessors.append(block) + term.target_blocks = [ + blocks[i + 1] if i + 1 < len(blocks) else false_block, + false_block, + ] elif term.op == Op.TRY_BEGIN: # TRY_BEGIN: fall through to try body, jump to except on exception @@ -1697,6 +2086,22 @@ def _build_cfg_edges(self, func: Function): # RETURN/RETURN_VOID have no successors + # Materialize implicit fallthrough as an explicit JUMP. This makes the + # CFG self-contained for cloning/analysis passes instead of relying on + # block list order. + for i, block in enumerate(blocks): + if block.terminator is None and i + 1 < len(blocks): + succ = blocks[i + 1] + if succ not in block.successors: + block.successors.append(succ) + if block not in succ.predecessors: + succ.predecessors.append(block) + jump = Instruction(Op.JUMP, [], None, None) + jump.imm_str = succ.label + jump.target_blocks = [succ] + jump.block = block + block.append(jump) + # ── Phase 6: SSA construction ─────────────────────────────── def _construct_ssa(self, func: Function): @@ -1826,6 +2231,25 @@ def _construct_ssa(self, func: Function): ) changed = True + # Loop exits can inherit the back-edge value even though that value is + # not defined on the exit path. Re-point each successor's incoming + # version at the header PHI when the old value is not defined there. + for block in func.blocks: + for phi in block.phi_nodes: + var_idx = phi.imm_int + if phi.result is None: + continue + for succ in block.successors: + incoming = block_entry_vars.get(succ.label, {}).get(var_idx) + if incoming is None or incoming is phi.result: + continue + defining = getattr(incoming, "defining_inst", None) + if defining is not None and defining.block is succ: + continue + self._replace_val_in_block_and_succs( + func, succ, incoming, phi.result, block_entry_vars, block_exit_vars + ) + def _replace_val_in_block_and_succs( self, func, start_block, old_val, new_val, block_entry_vars, block_exit_vars ): diff --git a/src/optimizer/passes.py b/src/optimizer/passes.py index 3f88210..0bd1c64 100644 --- a/src/optimizer/passes.py +++ b/src/optimizer/passes.py @@ -35,6 +35,8 @@ ) from optimizer.analysis import DomTree, LoopInfo, LivenessInfo, FunctionAnalysis from optimizer.types import EscapeAnalysis +from optimizer.config import OptimizerConfig +from optimizer.ranges import analyze_module_ranges, range_fold_branches # ── SCCP: Sparse Conditional Constant Propagation ─────────────── @@ -111,6 +113,8 @@ def _try_fold(inst: Instruction): if op == Op.MOD and len(const_vals) == 2 and const_vals[1] != 0: return (const_vals[0] % const_vals[1], IRType.INT64) if op == Op.NEG and len(const_vals) == 1: + if inst.operands and inst.operands[0].type == IRType.FLOAT64: + return (-float(const_vals[0]), IRType.FLOAT64) return (-const_vals[0], IRType.INT64) # Float arithmetic @@ -215,6 +219,12 @@ def _try_simplify(inst: Instruction) -> bool | None: op = inst.op ops = inst.operands + # Redundant conversions (mostly exposed by list element type propagation). + if op in (Op.FLOAT_TO_INT, Op.INT_TO_FLOAT, Op.TO_BOOL) and ops and inst.result is not None: + if ops[0].type == inst.result.type: + _replace_with_value(inst, ops[0]) + return True + # x + 0 → x if op == Op.ADD and len(ops) == 2: c1 = _get_const_value(ops[1]) @@ -387,6 +397,15 @@ def _make_const_int(val: int, inst: Instruction) -> Value: # ── DCE: Dead Code Elimination ───────────────────────────────── +def rebuild_uses(func: Function) -> None: + for value in func.all_values(): + value.uses = [] + for inst in func.all_instructions(): + for operand in inst.operands: + if isinstance(operand, Value) and inst not in operand.uses: + operand.uses.append(inst) + + def dce(func: Function) -> bool: """Remove instructions whose results are unused and that have no side effects.""" changed = False @@ -474,7 +493,7 @@ def cse(func: Function) -> bool: def _cse_key(inst: Instruction) -> tuple | None: """Create a hashable key for an instruction for CSE. Returns None if the instruction can't be CSE'd.""" - if inst.op in SIDE_EFFECT_OPS: + if inst.has_side_effects: return None # Don't CSE allocations if inst.op in (Op.ALLOC_STRUCT, Op.ALLOC_LIST): @@ -530,10 +549,107 @@ def _find_preheader(loop: Loop) -> BasicBlock | None: return None +def merge_linear_blocks(func: Function) -> bool: + """Merge A -> B when B has exactly one predecessor. + + This removes the artificial fallthrough/JUMP blocks created by the SSA + builder and exposes straight-line bodies to the inliner and peephole pass. + """ + changed = False + for block in func.blocks: + seen = set() + block.predecessors = [ + p for p in block.predecessors if not (id(p) in seen or seen.add(id(p))) + ] + seen = set() + block.successors = [ + s for s in block.successors if not (id(s) in seen or seen.add(id(s))) + ] + + while True: + merged = False + for a in list(func.blocks): + term = a.terminator + if term is None or term.op != Op.JUMP or len(term.target_blocks) != 1: + continue + b = term.target_blocks[0] + if b is a or b is func.entry: + continue + if len(b.predecessors) != 1 or b.predecessors[0] is not a: + continue + if b.phi_nodes: + continue + # Do not merge into a loop header with a back edge. + if any(succ is b for succ in b.successors): + continue + + a.instructions.pop() # remove JUMP + for inst in b.instructions: + inst.block = a + a.instructions.append(inst) + if b.terminator is None and len(b.successors) == 1: + # Materialize the implicit fallthrough edge as a real JUMP. + jump = Instruction(Op.JUMP, [], None, None) + jump.imm_str = b.successors[0].label + jump.target_blocks = [b.successors[0]] + a.instructions.append(jump) + a.successors = list(b.successors) + for succ in a.successors: + succ.predecessors = [a if p is b else p for p in succ.predecessors] + # PHIs are indexed by predecessor block; re-point entries that + # referred to the merged block. + for phi in succ.phi_nodes: + phi.target_blocks = [ + a if pred is b else pred for pred in phi.target_blocks + ] + func.blocks.remove(b) + changed = True + merged = True + break + if not merged: + break + return changed + + +def remove_unreachable_blocks(func: Function) -> bool: + """Delete blocks that cannot be reached from the entry block.""" + if func.entry is None: + return False + reachable = set() + worklist = [func.entry] + while worklist: + block = worklist.pop() + if block in reachable: + continue + reachable.add(block) + worklist.extend(block.successors) + + if len(reachable) == len(func.blocks): + return False + + for block in list(func.blocks): + if block not in reachable: + func.blocks.remove(block) + + for block in func.blocks: + block.successors = [b for b in block.successors if b in reachable] + block.predecessors = [b for b in block.predecessors if b in reachable] + + # Rebuild target_blocks from the (possibly rewritten) terminators. + for block in func.blocks: + term = block.terminator + if term is not None: + term.target_blocks = [b for b in term.target_blocks if b in reachable] + block.successors = list(term.target_blocks) + if term.op == Op.JUMP and not term.target_blocks: + term.target_blocks = [] + return True + + # ── SROA: Scalar Replacement of Aggregates ────────────────────── -def sroa(func: Function) -> bool: +def sroa(func: Function, escape: "EscapeAnalysis | None" = None) -> bool: """Replace struct allocations with individual scalar values when possible. A struct can be SROA'd if: @@ -544,7 +660,9 @@ def sroa(func: Function) -> bool: MAX_SROA_FIELDS = 8 changed = False - escape = EscapeAnalysis(func) + if escape is None: + escape = EscapeAnalysis(func) + escape.run() for block in func.blocks: for inst in list(block.instructions): @@ -794,10 +912,747 @@ def simplify_branches(func: Function) -> bool: return changed +# ── Type propagation for generic list types ───────────────────── + +def propagate_list_types(module: Module) -> bool: + """Refine generic ``list`` parameter/return/field types from call sites. + + The bytecode type name only says ``list``; element types appear later in + the IR (literals, split results, field constructors). Propagating them + makes ``contains``, printing and list element access use the right ABI. + """ + funcs = {f.name: f for f in module.functions} + changed_any = False + + def refine(current, incoming): + if not isinstance(current, ListType) or not isinstance(incoming, ListType): + return current, False + if current.elem_type == IRType.INT64 and incoming.elem_type != IRType.INT64: + return incoming, True + return current, False + + for _ in range(8): + changed = False + # Call-site parameters. + for func in module.functions: + for inst in func.all_instructions(): + if inst.op != Op.CALL or not inst.imm_str: + continue + target = funcs.get(inst.imm_str) + if target is None: + continue + for param, arg in zip(target.params, inst.operands): + new_type, did = refine(param.type, arg.type) + if did: + param.type = new_type + changed = True + + # Instruction results and generic list mutations. + for func in module.functions: + for inst in func.all_instructions(): + if inst.op in (Op.LIST_GET,) and inst.result is not None: + lst = inst.operands[0] if inst.operands else None + if isinstance(lst, Value) and isinstance(lst.type, ListType): + elem = lst.type.elem_type + if inst.op == Op.LIST_GET and inst.result.type != elem: + inst.result.type = elem + changed = True + elif inst.op == Op.PHI and inst.result is not None and inst.operands: + operand_types = [op.type for op in inst.operands if isinstance(op, Value)] + if operand_types: + new_type = operand_types[0] + if new_type == IRType.INT64: + for t in operand_types[1:]: + if t != IRType.INT64: + new_type = t + break + if inst.result.type != new_type: + inst.result.type = new_type + changed = True + elif inst.op == Op.LIST_APPEND and len(inst.operands) >= 2: + lst, val = inst.operands[0], inst.operands[1] + new_type, did = refine(lst.type, ListType(val.type)) + if did: + lst.type = new_type + changed = True + elif ( + inst.op == Op.CALL_BUILTIN + and inst.imm_str in ("contains", "str_contains") + and len(inst.operands) >= 1 + ): + container = inst.operands[0] + new_name = ( + "str_contains" if container.type == IRType.STRING else "contains" + ) + if inst.imm_str != new_name: + inst.imm_str = new_name + changed = True + elif inst.op == Op.LOAD_FIELD and inst.result is not None: + base = inst.operands[0] if inst.operands else None + if isinstance(base, Value) and isinstance(base.type, StructType): + idx = inst.imm_int or 0 + if idx < len(base.type.field_types): + field_type = base.type.field_types[idx] + if inst.result.type != field_type: + inst.result.type = field_type + changed = True + if ( + isinstance(field_type, ListType) + and isinstance(inst.result.type, ListType) + and field_type.elem_type == IRType.INT64 + and inst.result.type.elem_type != IRType.INT64 + ): + base.type.field_types[idx] = inst.result.type + changed = True + elif inst.op == Op.ALLOC_STRUCT and inst.result is not None: + if isinstance(inst.result.type, StructType): + for idx, operand in enumerate(inst.operands): + if idx >= len(inst.result.type.field_types): + break + field_type = inst.result.type.field_types[idx] + if ( + isinstance(field_type, ListType) + and isinstance(operand.type, ListType) + and field_type.elem_type == IRType.INT64 + and operand.type.elem_type != IRType.INT64 + ): + inst.result.type.field_types[idx] = operand.type + changed = True + + # Return types and RETURN operand refinement. + for func in module.functions: + if func.is_extern: + continue + for inst in func.all_instructions(): + if inst.op != Op.RETURN or not inst.operands: + continue + new_type, did = refine(func.return_type, inst.operands[0].type) + if did: + func.return_type = new_type + # Existing call sites already carry the old generic return + # type; update them so the new element type propagates + # through locals and struct fields. + for caller in module.functions: + for call in caller.all_instructions(): + if ( + call.op == Op.CALL + and call.imm_str == func.name + and call.result is not None + ): + call.result.type = new_type + changed = True + + if not changed: + break + changed_any = True + return changed_any + + +# ── Side-effect / purity analysis ─────────────────────────────── + +# Builtins that cannot observe or mutate program state. Calls to these are +# safe to eliminate when their result is unused and safe to CSE. +_PURE_BUILTINS = frozenset({ + "str", "int", "float", "bool", "len", "abs", "min", "max", + "sqrt", "pow", "sin", "cos", "tan", "floor", "ceil", "round", + "contains", "split", "join", "replace", "strip", "upper", "lower", + "get_char", "repeat", "encode", "decode", +}) + +_IMPURE_OPS = frozenset({ + Op.PRINT, Op.PRINTLN, Op.INPUT, Op.STORE_GLOBAL, + Op.STORE_FIELD, Op.LOAD_FIELD, Op.LIST_SET, Op.LIST_APPEND, + Op.LIST_GET, Op.PTR_STORE, Op.PTR_LOAD, Op.LOAD_GLOBAL, + Op.DIV, Op.MOD, Op.FDIV, Op.LIST_ELEM_ADDR, + Op.RAISE, Op.TRY_BEGIN, Op.TRY_END, +}) + + +def mark_purity(module: Module) -> bool: + """Compute side-effect purity through the call graph. + + A function is pure when every instruction is pure and every callee is + pure. Starting from "pure" and lowering to a fixed point gives the + greatest fixed point, so recursive pure functions are handled correctly. + """ + funcs = {f.name: f for f in module.functions} + purity = {name: (not f.is_extern) for name, f in funcs.items()} + changed = True + while changed: + changed = False + for name, func in funcs.items(): + if not purity.get(name, False): + continue + pure = True + for inst in func.all_instructions(): + if inst.op in _IMPURE_OPS: + pure = False + break + if inst.op == Op.CALL: + callee = inst.imm_str + if not callee or not purity.get(callee, False): + pure = False + break + elif inst.op == Op.CALL_BUILTIN: + if (inst.imm_str or "") not in _PURE_BUILTINS: + pure = False + break + if not pure: + purity[name] = False + changed = True + + flag_changed = False + for func in module.functions: + for inst in func.all_instructions(): + if inst.op not in (Op.CALL, Op.CALL_BUILTIN): + continue + if inst.op == Op.CALL: + pure = purity.get(inst.imm_str or "", False) + else: + pure = (inst.imm_str or "") in _PURE_BUILTINS + if inst.pure_call != pure: + inst.pure_call = pure + flag_changed = True + return flag_changed + + +# ── Call-site specialization ──────────────────────────────────── + +_SPECIALIZATION_LIMIT = 64 + + +def _const_info(value: Value): + """Return (type, value) for a constant SSA value, or None.""" + if isinstance(value, Constant): + return (value.type, value.value) + inst = value.defining_inst if isinstance(value, Value) else None + if inst is None: + return None + if inst.op == Op.CONST_INT: + return (IRType.INT64, inst.imm_int) + if inst.op == Op.CONST_FLOAT: + return (IRType.FLOAT64, inst.imm_float) + if inst.op == Op.CONST_BOOL: + return (IRType.BOOL, bool(inst.imm_int)) + if inst.op == Op.CONST_STR: + return (IRType.STRING, inst.imm_str) + return None + + +def _arg_spec(value: Value, range_hint, config: OptimizerConfig): + """Describe what a call site knows about one argument. + + Specs are ('const', type, value), ('range', lo, hi) or ('any',). + """ + info = _const_info(value) + if info is not None: + return ("const", info[0], info[1]) + r = range_hint + if r is not None: + lo, hi = int(r[0]), int(r[1]) + if hi - lo <= config.max_specialized_range: + if lo == hi: + return ("const", IRType.INT64, lo) + return ("range", lo, hi) + return ("any",) + + +def _spec_key(spec) -> tuple: + if spec[0] == "const": + return ("c", str(spec[1]), repr(spec[2])) + if spec[0] == "range": + return ("r", spec[1], spec[2]) + return ("u",) + + +def _clone_specialized( + module: Module, + target: Function, + specs: list, + new_name: str, + config: OptimizerConfig | None = None, +) -> Function: + """Clone *target* with call-site constants and range constraints.""" + cfg = config or OptimizerConfig.from_env() + params = [Param(p.name, p.type, i) for i, p in enumerate(target.params)] + clone = Function(new_name, params, target.return_type) + clone.source_file = target.source_file + clone.local_names = dict(target.local_names) + + # Constant specializations get a tiny synthetic entry block. Putting the + # constants in the cloned entry would shift instruction indices and break + # PHI/target alignment. + has_consts = any(spec[0] == "const" for spec in specs) + const_entry = None + if has_consts: + const_entry = clone.add_block(BasicBlock("__spec_entry")) + + block_map: dict[int, BasicBlock] = {} + for old_block in target.blocks: + block_map[id(old_block)] = clone.add_block(BasicBlock(old_block.label)) + + value_map: dict[int, Value] = {} + for old_param, new_param, spec in zip(target.params, params, specs): + if spec[0] == "const": + _, ctype, cvalue = spec + const_op = { + IRType.STRING: Op.CONST_STR, + IRType.FLOAT64: Op.CONST_FLOAT, + IRType.BOOL: Op.CONST_BOOL, + }.get(ctype, Op.CONST_INT) + inst = Instruction(const_op, [], ctype, None) + if ctype == IRType.INT64: + inst.imm_int = int(cvalue) + elif ctype == IRType.FLOAT64: + inst.imm_float = float(cvalue) + elif ctype == IRType.BOOL: + inst.imm_int = 1 if cvalue else 0 + elif ctype == IRType.STRING: + inst.imm_str = str(cvalue) + if cvalue not in module.string_constants: + module.string_constants.append(cvalue) + inst.block = const_entry + const_entry.instructions.append(inst) + if inst.result: + value_map[old_param.id] = inst.result + if ctype in (IRType.INT64, IRType.BOOL): + clone.param_ranges[new_param.id] = ( + (int(cvalue), int(cvalue)) + if ctype == IRType.INT64 + else ((1, 1) if cvalue else (0, 0)) + ) + else: + value_map[old_param.id] = new_param + if spec[0] == "range": + clone.param_ranges[new_param.id] = (spec[1], spec[2]) + + if const_entry is not None and target.entry is not None: + target_entry = block_map[id(target.entry)] + jump = Instruction(Op.JUMP, [], None, None) + jump.imm_str = target_entry.label + jump.target_blocks = [target_entry] + jump.block = const_entry + const_entry.instructions.append(jump) + + # Two-pass clone: first allocate every result value, then rewrite + # operands. PHIs reference values defined later in the function, so a + # single pass would leave stale cross-function references. + inst_map: dict[int, Instruction] = {} + for old_block in target.blocks: + new_block = block_map[id(old_block)] + for old_inst in old_block.instructions: + new_inst = Instruction( + old_inst.op, + [], + old_inst.result.type if old_inst.result else None, + old_inst.source_line, + ) + new_inst.imm_int = old_inst.imm_int + new_inst.imm_str = old_inst.imm_str + new_inst.imm_float = old_inst.imm_float + new_inst.stack_alloc = old_inst.stack_alloc + new_block.append(new_inst) + inst_map[id(old_inst)] = new_inst + if old_inst.result and new_inst.result: + value_map[old_inst.result.id] = new_inst.result + + for old_block in target.blocks: + new_block = block_map[id(old_block)] + for old_inst, new_inst in zip(old_block.instructions, new_block.instructions): + new_inst.operands = [ + value_map.get(operand.id, operand) + if isinstance(operand, Value) + else operand + for operand in old_inst.operands + ] + for operand in new_inst.operands: + if isinstance(operand, Value) and new_inst not in operand.uses: + operand.uses.append(new_inst) + if old_inst.target_blocks: + new_inst.target_blocks = [ + block_map[id(b)] for b in old_inst.target_blocks if id(b) in block_map + ] + + for block in clone.blocks: + block.predecessors = [] + block.successors = [] + for block in clone.blocks: + term = block.terminator + if term is None: + continue + for succ in term.target_blocks: + block.successors.append(succ) + if block not in succ.predecessors: + succ.predecessors.append(block) + + # Fold the newly exposed constants/ranges and prune unreachable branches. + for _ in range(4): + c1 = sccp(clone) + c2 = algebraic_simplify(clone) + c3 = simplify_branches(clone) + c4 = range_fold_branches(clone, clone.param_ranges, cfg) + c5 = remove_unreachable_blocks(clone) + c6 = merge_linear_blocks(clone) + c7 = dce(clone) + if not (c1 or c2 or c3 or c4 or c5 or c6 or c7): + break + + return clone + + +def specialize_calls( + module: Module, config: OptimizerConfig | None = None +) -> bool: + """Create a call-specific version of small functions. + + Specialization uses both constant arguments and ranges inferred by the + interprocedural analysis. Dead branches and their side effects disappear + from each clone, and single-block clones are subsequently inlined by the + normal inliner (effectively merging the function into its caller). + """ + cfg = config or OptimizerConfig.from_env() + changed = False + cache: dict[tuple, str] = {} + specializations = 0 + + for func in list(module.functions): + for block in func.blocks: + for inst in block.instructions: + if inst.op != Op.CALL or not inst.imm_str: + continue + target = module.get_function(inst.imm_str) + if target is None or target is func or target.is_extern: + continue + if "__spec_" in target.name: + continue + # Recursive/mutually-recursive functions must not be cloned + # per call: each clone would recurse with a new constant and + # create an unbounded specialization chain. + if any( + call.op == Op.CALL and call.imm_str == target.name + for call in target.all_instructions() + ): + continue + if len(target.blocks) < 2: + continue + if sum(len(b.instructions) for b in target.blocks) > cfg.max_specialized_body: + continue + if len(inst.operands) != len(target.params): + continue + + hint_ranges = inst.arg_ranges or [None] * len(inst.operands) + specs = [ + _arg_spec(arg, hint_ranges[i] if i < len(hint_ranges) else None, cfg) + for i, arg in enumerate(inst.operands) + ] + if all(spec[0] == "any" for spec in specs): + continue + + key = (target.name, tuple(_spec_key(spec) for spec in specs)) + new_name = cache.get(key) + if new_name is None: + if specializations >= cfg.max_specializations: + continue + specializations += 1 + candidate = f"{target.name}__spec_{specializations}" + clone = _clone_specialized(module, target, specs, candidate, cfg) + target_size = sum(len(b.instructions) for b in target.blocks) + clone_size = sum(len(b.instructions) for b in clone.blocks) + beneficial = ( + len(clone.blocks) < len(target.blocks) + or clone_size < target_size + ) + if not beneficial: + cache[key] = "" + continue + new_name = candidate + module.add_function(clone) + cache[key] = new_name + if not new_name: + continue + if inst.imm_str != new_name: + inst.imm_str = new_name + changed = True + + return changed + + +def licm(func: Function) -> bool: + """Hoist loop-invariant, side-effect-free instructions to the entry block. + + Constants and parameters (and values already in entry) are available + everywhere, so they are safe to move; memory reads and calls are never + hoisted. + """ + if func.entry is None: + return False + try: + loops = FunctionAnalysis(func).loops.loops + except Exception: + return False + if not loops: + return False + + entry = func.entry + changed = False + for loop in loops: + # Values available at entry: params/constants/entry-defined. + invariant: set[int] = set() + for block in func.blocks: + if block is entry: + for inst in block.instructions: + if inst.result is not None: + invariant.add(inst.result.id) + for param in func.params: + invariant.add(param.id) + + hoisted: list[Instruction] = [] + for block in loop.blocks: + if block is entry: + continue + for inst in list(block.instructions): + if inst.result is None or inst.op in ( + Op.PHI, Op.JUMP, Op.BRANCH, Op.RETURN, Op.RETURN_VOID, + Op.CONST_INT, Op.CONST_FLOAT, Op.CONST_STR, Op.CONST_BOOL, + ): + continue + if inst.has_side_effects or inst.op in ( + Op.ALLOC_LIST, Op.ALLOC_STRUCT, Op.ALLOCA, + Op.CALL, Op.CALL_BUILTIN, Op.CALL_EXTERN, + Op.DIV, Op.MOD, Op.FDIV, + ): + continue + if all( + isinstance(op, Value) and op.id in invariant + for op in inst.operands + ): + block.instructions.remove(inst) + inst.block = entry + insert_at = len(entry.instructions) + if entry.terminator is not None: + insert_at -= 1 + entry.instructions.insert(insert_at, inst) + invariant.add(inst.result.id) + hoisted.append(inst) + changed = True + return changed + + +def remove_unused_functions(module: Module) -> bool: + """Remove functions unreachable from the entry point.""" + by_name = {f.name: f for f in module.functions} + roots = [] + if module.entry_func and module.entry_func in by_name: + roots.append(by_name[module.entry_func]) + elif "main" in by_name: + roots.append(by_name["main"]) + roots.extend(f for f in module.functions if f.is_extern) + if not roots: + return False + + reachable: set[str] = set() + worklist = list(roots) + while worklist: + func = worklist.pop() + if func.name in reachable: + continue + reachable.add(func.name) + for inst in func.all_instructions(): + if inst.op == Op.CALL and inst.imm_str: + callee = by_name.get(inst.imm_str) + if callee is not None and callee.name not in reachable: + worklist.append(callee) + + before = len(module.functions) + module.functions = [f for f in module.functions if f.name in reachable] + return len(module.functions) != before + + +def fold_pure_constant_calls( + module: Module, config: OptimizerConfig | None = None +) -> bool: + """Evaluate pure calls with constant arguments at compile time.""" + from optimizer.puresim import evaluate_function, _UNKNOWN + + cfg = config or OptimizerConfig.from_env() + funcs = {f.name: f for f in module.functions} + changed = False + for func in module.functions: + for block in func.blocks: + for inst in list(block.instructions): + if inst.op != Op.CALL or not inst.imm_str: + continue + target = funcs.get(inst.imm_str) + if target is None or not _is_pure_function(target): + continue + if len(inst.operands) != len(target.params): + continue + args = [] + ok = True + for operand in inst.operands: + info = _const_info(operand) + if info is None or info[0] not in ( + IRType.INT64, IRType.FLOAT64, IRType.BOOL, IRType.STRING + ): + ok = False + break + args.append(info[1]) + if not ok or inst.result is None: + continue + result = evaluate_function(module, target, args, cfg) + if result is _UNKNOWN: + continue + if isinstance(result, bool): + inst.op = Op.CONST_BOOL + inst.imm_int = 1 if result else 0 + inst.result.type = IRType.BOOL + elif isinstance(result, int): + inst.op = Op.CONST_INT + inst.imm_int = result + inst.result.type = IRType.INT64 + elif isinstance(result, float): + inst.op = Op.CONST_FLOAT + inst.imm_float = result + inst.result.type = IRType.FLOAT64 + elif isinstance(result, str): + inst.op = Op.CONST_STR + inst.imm_str = result + inst.result.type = IRType.STRING + if result not in module.string_constants: + module.string_constants.append(result) + else: + continue + for operand in inst.operands: + if isinstance(operand, Value) and inst in operand.uses: + operand.uses.remove(inst) + inst.operands = [] + changed = True + return changed + + +def _is_pure_function(func: Function) -> bool: + if func.is_extern: + return False + for inst in func.all_instructions(): + if inst.op in _IMPURE_OPS: + return False + if inst.op in (Op.CALL, Op.CALL_BUILTIN) and not inst.pure_call: + return False + if inst.op in (Op.CALL_EXTERN, Op.TRY_BEGIN, Op.TRY_END): + return False + return True + + +def build_lookup_tables( + module: Module, config: OptimizerConfig | None = None +) -> bool: + """Precompute hot results of pure scalar functions. + + Each use site contributes its constant value or inferred range. Those + values are simulated at compile time (with a hard step/time budget) and + embedded in read-only memory as a small lookup table checked on entry. + Misses fall through to the normal function body, so this optimization is + always semantics-preserving. + """ + from optimizer.puresim import evaluate_function, _UNKNOWN + + cfg = config or OptimizerConfig.from_env() + funcs = {f.name: f for f in module.functions} + candidates: dict[str, set[int]] = defaultdict(set) + + for func in module.functions: + for inst in func.all_instructions(): + if inst.op != Op.CALL or not inst.imm_str: + continue + target = funcs.get(inst.imm_str) + if target is None: + continue + hint_ranges = inst.arg_ranges or [] + for i, arg in enumerate(inst.operands): + if i != 0: + continue # one-argument tables for now + info = _const_info(arg) + if info is not None and info[0] in (IRType.INT64, IRType.BOOL): + candidates[target.name].add(int(info[1])) + continue + r = hint_ranges[i] if i < len(hint_ranges) else None + if r is not None: + lo, hi = int(r[0]), int(r[1]) + if hi - lo + 1 <= cfg.max_table_domain: + for v in range(lo, min(hi, lo + cfg.max_table_entries - 1) + 1): + candidates[target.name].add(v) + + changed = False + total_queries = 0 + for name, values in candidates.items(): + func = funcs.get(name) + if func is None or not _is_pure_function(func): + continue + if len(func.params) != 1 or func.return_type not in ( + IRType.INT64, + IRType.BOOL, + ): + continue + param_type = func.params[0].type + if param_type not in (IRType.INT64, IRType.BOOL): + continue + if sum(len(b.instructions) for b in func.blocks) > cfg.max_table_function_size: + continue + if len(values) < cfg.min_table_entries: + continue + + lo = min(values) + hi = max(values) + # Densify small observed windows: a contiguous table is cheaper than a + # compare chain and still bounded by the configured budget. + if hi - lo + 1 <= cfg.max_table_domain and hi - lo + 1 <= cfg.max_table_entries: + values = set(range(lo, hi + 1)) + if len(values) > cfg.max_table_entries: + values = set(sorted(values)[: cfg.max_table_entries]) + + entries: dict[int, int] = {} + for arg in sorted(values): + total_queries += 1 + if total_queries > cfg.max_sim_calls: + break + result = evaluate_function(module, func, [arg], cfg) + if result is _UNKNOWN: + continue + if isinstance(result, bool): + entries[int(arg)] = 1 if result else 0 + elif isinstance(result, int): + entries[int(arg)] = int(result) + elif isinstance(result, float) and result.is_integer(): + entries[int(arg)] = int(result) + else: + continue + if len(entries) < cfg.min_table_entries: + continue + + ordered = sorted(entries) + if ordered and ordered[-1] - ordered[0] + 1 == len(ordered): + func.lookup_table = { + "kind": "dense", + "min": ordered[0], + "max": ordered[-1], + "values": [entries[v] for v in ordered], + } + else: + func.lookup_table = { + "kind": "sparse", + "pairs": [(v, entries[v]) for v in ordered], + } + changed = True + + return changed + + # ── Pass pipeline ─────────────────────────────────────────────── -def run_passes(module: Module, level: int = 2) -> bool: +def run_passes( + module: Module, level: int = 2, config: OptimizerConfig | None = None +) -> bool: """Run optimization passes on the module. level 0: no optimization @@ -808,11 +1663,25 @@ def run_passes(module: Module, level: int = 2) -> bool: if level == 0: return False + cfg = config or OptimizerConfig.from_env() + propagate_list_types(module) overall_changed = False + # Escape analysis is interprocedural-ish (it summarizes call sites), so + # compute it once per fixed-point iteration instead of passing a Function + # where a Module is expected (the previous code made SROA a no-op). + escape = EscapeAnalysis(module) + escape.run() + mark_purity(module) + + # Whole-program constraint propagation: call-site values/range facts flow + # into callee parameters and are recorded on each call for specialization. + if level >= 2: + analyze_module_ranges(module, cfg) + # Run passes in a fixed-point loop - for iteration in range(20): # Safety limit - changed = False + for iteration in range(cfg.max_iterations): + changed = propagate_list_types(module) for func in module.functions: if func.is_extern: @@ -821,15 +1690,29 @@ def run_passes(module: Module, level: int = 2) -> bool: changed |= sccp(func) changed |= algebraic_simplify(func) changed |= simplify_branches(func) + if level >= 2: + changed |= range_fold_branches(func, func.param_ranges, cfg) + changed |= remove_unreachable_blocks(func) + changed |= merge_linear_blocks(func) changed |= dce(func) if level >= 2: changed |= cse(func) - changed |= sroa(func) + changed |= licm(func) + changed |= sroa(func, escape) changed |= tco(func) if level >= 2: - changed |= inline_small_functions(module) + changed |= inline_small_functions(module, cfg.inline_max_inst) + changed |= specialize_calls(module, cfg) + for func in module.functions: + if not func.is_extern: + rebuild_uses(func) + changed |= mark_purity(module) + changed |= fold_pure_constant_calls(module, cfg) + if changed: + analyze_module_ranges(module, cfg) + changed |= mark_purity(module) overall_changed |= changed if not changed: @@ -838,6 +1721,21 @@ def run_passes(module: Module, level: int = 2) -> bool: # Final DCE cleanup for func in module.functions: if not func.is_extern: + rebuild_uses(func) dce(func) + remove_unused_functions(module) + if level >= 2: + build_lookup_tables(module, cfg) + + # Mark allocations that never escape the function for stack placement. + # SROA already removed the fully-local ones; this covers structs that are + # only passed to callees (ARG_ESCAPE) and therefore still need an address. + escape = EscapeAnalysis(module) + escape.run() + for func in module.functions: + for inst in func.all_instructions(): + if inst.op == Op.ALLOC_STRUCT and inst.result is not None: + inst.stack_alloc = escape.can_stack_alloc(inst.result) + return overall_changed diff --git a/src/optimizer/puresim.py b/src/optimizer/puresim.py new file mode 100644 index 0000000..af186f2 --- /dev/null +++ b/src/optimizer/puresim.py @@ -0,0 +1,290 @@ +"""Budgeted compile-time interpreter for pure scalar IR functions. + +This is used to populate lookup tables and constant-return summaries. It is a +best-effort analysis: any unsupported operation, unknown value or budget +exhaustion simply aborts the query and leaves the function untouched. +""" + +from __future__ import annotations + +import math +import time + +from optimizer.config import OptimizerConfig +from optimizer.ir import ( + Constant, + Function, + Module, + Op, + IRType, + Value, +) + +_UNKNOWN = object() + + +class SimulationError(Exception): + pass + + +class SimulationBudget: + def __init__(self, config: OptimizerConfig): + self.config = config + self.steps = 0 + self.calls = 0 + self.deadline = time.monotonic() + config.max_sim_time_ms / 1000.0 + + def tick(self) -> None: + self.steps += 1 + if self.steps > self.config.max_sim_steps: + raise SimulationError("simulation step budget exhausted") + if (self.steps & 255) == 0 and time.monotonic() > self.deadline: + raise SimulationError("simulation time budget exhausted") + + def call(self) -> None: + self.calls += 1 + if self.calls > self.config.max_sim_calls: + raise SimulationError("simulation call budget exhausted") + + +def _value(v, env: dict[int, object]): + if isinstance(v, Constant): + return v.value + if isinstance(v, Value): + if v.id in env: + return env[v.id] + inst = v.defining_inst + if inst is not None: + if inst.op == Op.CONST_INT: + return int(inst.imm_int or 0) + if inst.op == Op.CONST_BOOL: + return bool(inst.imm_int) + if inst.op == Op.CONST_FLOAT: + return float(inst.imm_float or 0.0) + if inst.op == Op.CONST_STR: + return inst.imm_str or "" + raise SimulationError(f"unknown value {v}") + return v + + +class PureInterpreter: + def __init__(self, module: Module, config: OptimizerConfig): + self.module = module + self.config = config + self.budget = SimulationBudget(config) + self.depth = 0 + + def run(self, func: Function, args: list) -> object: + self.budget.call() + if self.depth > 32: + raise SimulationError("recursion depth budget exhausted") + if func.entry is None: + raise SimulationError("function has no entry") + env: dict[int, object] = {} + for param, arg in zip(func.params, args): + env[param.id] = arg + + block = func.entry + pred = None + steps = 0 + while block is not None: + steps += 1 + if steps > 10000: + raise SimulationError("block execution budget exhausted") + next_block = None + new_pred = block + for inst in block.instructions: + self.budget.tick() + if inst.op == Op.PHI: + if inst.result is None: + continue + chosen = None + for i, operand in enumerate(inst.operands): + if i < len(inst.target_blocks) and inst.target_blocks[i] is pred: + chosen = operand + break + if chosen is None and inst.operands: + chosen = inst.operands[0] + if chosen is None: + raise SimulationError("phi has no inputs") + env[inst.result.id] = _value(chosen, env) + continue + + if inst.op == Op.JUMP: + next_block = inst.target_blocks[0] if inst.target_blocks else None + break + if inst.op == Op.BRANCH: + cond = bool(_value(inst.operands[0], env)) + if len(inst.target_blocks) < 2: + raise SimulationError("branch targets missing") + next_block = inst.target_blocks[0] if cond else inst.target_blocks[1] + break + if inst.op == Op.RETURN: + return _value(inst.operands[0], env) if inst.operands else None + if inst.op == Op.RETURN_VOID: + return None + if inst.op in (Op.TRY_BEGIN, Op.RAISE, Op.PRINT, Op.PRINTLN, Op.INPUT): + raise SimulationError("impure operation") + if inst.op in (Op.CALL_EXTERN,): + raise SimulationError("external call") + + result = self._eval(inst, env) + if inst.result is not None: + env[inst.result.id] = result + block = next_block + pred = new_pred + raise SimulationError("fell off function") + + def _eval(self, inst, env): + op = inst.op + args = [_value(v, env) for v in inst.operands] + + if op == Op.CONST_INT: + return int(inst.imm_int or 0) + if op == Op.CONST_BOOL: + return bool(inst.imm_int) + if op == Op.CONST_FLOAT: + return float(inst.imm_float or 0.0) + if op == Op.CONST_STR: + return inst.imm_str or "" + if op == Op.COPY: + return args[0] if args else None + if op == Op.NOP: + return None + if op == Op.ADD: + return int(args[0]) + int(args[1]) + if op == Op.SUB: + return int(args[0]) - int(args[1]) + if op == Op.MUL: + return int(args[0]) * int(args[1]) + if op == Op.DIV: + a, b = int(args[0]), int(args[1]) + if b == 0: + raise SimulationError("division by zero") + q = abs(a) // abs(b) + return q if (a < 0) == (b < 0) else -q + if op == Op.MOD: + a, b = int(args[0]), int(args[1]) + if b == 0: + raise SimulationError("division by zero") + q = abs(a) // abs(b) + q = q if (a < 0) == (b < 0) else -q + return a - q * b + if op == Op.NEG: + return -args[0] + if op in (Op.FADD, Op.FSUB, Op.FMUL, Op.FDIV, Op.FNEG): + a, b = (args + [0.0, 0.0])[:2] + if op == Op.FADD: + return float(a) + float(b) + if op == Op.FSUB: + return float(a) - float(b) + if op == Op.FMUL: + return float(a) * float(b) + if op == Op.FDIV: + return float(a) / float(b) + return -float(a) + if op in (Op.LT, Op.GT, Op.LE, Op.GE, Op.EQ, Op.NE): + a, b = args[0], args[1] + return { + Op.LT: a < b, + Op.GT: a > b, + Op.LE: a <= b, + Op.GE: a >= b, + Op.EQ: a == b, + Op.NE: a != b, + }[op] + if op == Op.AND: + return bool(args[0]) and bool(args[1]) + if op == Op.OR: + return bool(args[0]) or bool(args[1]) + if op == Op.NOT: + return not bool(args[0]) + if op == Op.SHL: + return int(args[0]) << int(args[1]) + if op == Op.SHR: + return int(args[0]) >> int(args[1]) + if op == Op.BIT_AND: + return int(args[0]) & int(args[1]) + if op == Op.BIT_OR: + return int(args[0]) | int(args[1]) + if op == Op.BIT_XOR: + return int(args[0]) ^ int(args[1]) + if op == Op.SELECT: + return args[1] if args[0] else args[2] + if op == Op.INT_TO_FLOAT: + return float(args[0]) + if op == Op.FLOAT_TO_INT: + return int(args[0]) + if op == Op.TO_BOOL: + return bool(args[0]) + if op == Op.ABS: + return abs(args[0]) + if op == Op.MIN: + return min(args[0], args[1]) + if op == Op.MAX: + return max(args[0], args[1]) + if op == Op.SQRT: + return math.sqrt(float(args[0])) + if op == Op.SIN: + return math.sin(float(args[0])) + if op == Op.COS: + return math.cos(float(args[0])) + if op == Op.TAN: + return math.tan(float(args[0])) + if op == Op.FLOOR: + return math.floor(float(args[0])) + if op == Op.CEIL: + return math.ceil(float(args[0])) + if op == Op.ROUND: + x = float(args[0]) + return math.floor(x + 0.5) if x >= 0 else math.ceil(x - 0.5) + if op == Op.POW: + return args[0] ** args[1] + if op == Op.CALL: + target = self.module.get_function(inst.imm_str or "") + if target is None or target.is_extern: + raise SimulationError("unknown callee") + self.depth += 1 + try: + return self.run(target, args) + finally: + self.depth -= 1 + if op == Op.CALL_BUILTIN: + name = inst.imm_str or "" + if name == "abs": + return abs(args[0]) + if name == "min": + return min(args[0], args[1]) + if name == "max": + return max(args[0], args[1]) + if name == "int": + return int(args[0]) + if name == "float": + return float(args[0]) + if name == "bool": + return bool(args[0]) + if name == "len" and isinstance(args[0], str): + return len(args[0]) + if name == "sqrt": + return math.sqrt(float(args[0])) + if name == "pow": + return args[0] ** args[1] + if name == "floor": + return math.floor(float(args[0])) + if name == "ceil": + return math.ceil(float(args[0])) + if name == "round": + x = float(args[0]) + return math.floor(x + 0.5) if x >= 0 else math.ceil(x - 0.5) + raise SimulationError(f"unsupported builtin {name}") + raise SimulationError(f"unsupported op {op}") + + +def evaluate_function( + module: Module, func: Function, args: list, config: OptimizerConfig +): + try: + interpreter = PureInterpreter(module, config) + return interpreter.run(func, args) + except (SimulationError, ArithmeticError, ValueError, TypeError, RecursionError): + return _UNKNOWN diff --git a/src/optimizer/ranges.py b/src/optimizer/ranges.py new file mode 100644 index 0000000..d65a0a7 --- /dev/null +++ b/src/optimizer/ranges.py @@ -0,0 +1,596 @@ +"""Budgeted interprocedural integer/boolean range analysis. + +The analysis is deliberately conservative: an unknown value has no entry in the +range map, and every arithmetic rule over-approximates. Its purpose is branch +pruning and specialization, never to replace runtime checks with an unsound +assumption. +""" + +from __future__ import annotations + +from collections import defaultdict, deque + +from optimizer.config import OptimizerConfig +from optimizer.ir import ( + BasicBlock, + Constant, + Function, + Instruction, + Module, + Op, + IRType, + Value, +) + +Range = tuple[int, int] +INT_MIN = -(1 << 62) +INT_MAX = (1 << 62) - 1 + + +def _rng(v: Value | Constant | None, ranges: dict[int, Range]) -> Range | None: + if v is None: + return None + if isinstance(v, Constant): + if isinstance(v.value, bool): + return (1, 1) if v.value else (0, 0) + if isinstance(v.value, int): + return (int(v.value), int(v.value)) + return None + if isinstance(v, (int, bool)): + return (int(v), int(v)) + if isinstance(v, Value): + r = ranges.get(v.id) + if r is not None: + return r + inst = v.defining_inst + if inst is not None: + if inst.op == Op.CONST_INT: + return (int(inst.imm_int or 0), int(inst.imm_int or 0)) + if inst.op == Op.CONST_BOOL: + return (1, 1) if inst.imm_int else (0, 0) + return None + return None + + +def _union(a: Range | None, b: Range | None) -> Range | None: + if a is None: + return b + if b is None: + return a + return (min(a[0], b[0]), max(a[1], b[1])) + + +def _bool_range(value: bool | None) -> Range | None: + if value is None: + return None + return (1, 1) if value else (0, 0) + + +def _compare_range(op: Op, a: Range, b: Range) -> Range: + # Exact comparison first (works for booleans and singleton ranges). + if a[0] == a[1] and b[0] == b[1]: + x, y = a[0], b[0] + result = { + Op.EQ: x == y, + Op.NE: x != y, + Op.LT: x < y, + Op.LE: x <= y, + Op.GT: x > y, + Op.GE: x >= y, + }.get(op) + if result is not None: + return _bool_range(result) # type: ignore[return-value] + + if op == Op.EQ: + if a[1] < b[0] or b[1] < a[0]: + return (0, 0) + return (0, 1) + if op == Op.NE: + if a[1] < b[0] or b[1] < a[0]: + return (1, 1) + return (0, 1) + if op == Op.LT: + if a[1] < b[0]: + return (1, 1) + if a[0] >= b[1]: + return (0, 0) + return (0, 1) + if op == Op.LE: + if a[1] <= b[0]: + return (1, 1) + if a[0] > b[1]: + return (0, 0) + return (0, 1) + if op == Op.GT: + if a[0] > b[1]: + return (1, 1) + if a[1] <= b[0]: + return (0, 0) + return (0, 1) + if op == Op.GE: + if a[0] >= b[1]: + return (1, 1) + if a[1] < b[0]: + return (0, 0) + return (0, 1) + return (0, 1) + + +def _arith_range(op: Op, a: Range, b: Range) -> Range | None: + try: + if op == Op.ADD: + return (a[0] + b[0], a[1] + b[1]) + if op == Op.SUB: + return (a[0] - b[1], a[1] - b[0]) + if op == Op.MUL: + vals = (a[0] * b[0], a[0] * b[1], a[1] * b[0], a[1] * b[1]) + return (min(vals), max(vals)) + if op in (Op.DIV, Op.MOD): + if b[0] <= 0 <= b[1]: + return None + if op == Op.DIV: + vals = ( + int(a[0] / b[0]), + int(a[0] / b[1]), + int(a[1] / b[0]), + int(a[1] / b[1]), + ) + return (min(vals), max(vals)) + # Remainder magnitude is bounded by the divisor. + bound = max(abs(b[0]), abs(b[1])) - 1 + return (-bound, bound) + if op == Op.MIN: + return (min(a[0], b[0]), min(a[1], b[1])) + if op == Op.MAX: + return (max(a[0], b[0]), max(a[1], b[1])) + except (OverflowError, ZeroDivisionError): + return None + return None + + +class RangeAnalysis: + def __init__( + self, + func: Function, + param_ranges: dict[int, Range] | None = None, + config: OptimizerConfig | None = None, + fixed_ranges: dict[int, Range] | None = None, + ): + self.func = func + self.config = config or OptimizerConfig.from_env() + self.fixed_ranges = fixed_ranges or {} + self.ranges: dict[int, Range] = dict(param_ranges or {}) + self.ranges.update(self.fixed_ranges) + self.visits: dict[int, int] = defaultdict(int) + self.total_visits = 0 + + def run(self) -> dict[int, Range]: + if self.func.entry is None: + return self.ranges + worklist: deque[BasicBlock] = deque([self.func.entry]) + queued: set[int] = {id(self.func.entry)} + + while worklist: + if ( + self.total_visits >= self.config.max_range_visits + or len(self.ranges) >= self.config.max_range_values + ): + break + block = worklist.popleft() + queued.discard(id(block)) + self.total_visits += 1 + self.visits[id(block)] += 1 + + changed = False + for inst in block.instructions: + result_range = self._infer_inst(inst) + if inst.result is None or result_range is None: + continue + old = self.ranges.get(inst.result.id) + new = _union(old, result_range) + # Widen values that keep changing inside a loop. + if self.visits[id(block)] > self.config.range_widen_after: + new = None if old is not None else old + if new != old: + if new is None: + self.ranges.pop(inst.result.id, None) + inst.result.value_range = None + else: + self.ranges[inst.result.id] = new + inst.result.value_range = new + changed = True + + successors = self._feasible_successors(block) + if changed or len(successors) < len(block.successors): + for succ in successors: + if id(succ) not in queued: + queued.add(id(succ)) + worklist.append(succ) + + for value in self.func.all_values(): + if value.id in self.ranges: + value.value_range = self.ranges[value.id] + return self.ranges + + def _infer_inst(self, inst: Instruction) -> Range | None: + op = inst.op + if op == Op.CONST_INT: + return (int(inst.imm_int or 0), int(inst.imm_int or 0)) + if op == Op.CONST_BOOL: + return (1, 1) if inst.imm_int else (0, 0) + if op == Op.PHI: + if inst.result is not None and inst.result.id in self.fixed_ranges: + return self.fixed_ranges[inst.result.id] + result = None + for operand in inst.operands: + r = _rng(operand, self.ranges) + if r is None: + return None + result = _union(result, r) + return result + if op == Op.SELECT and len(inst.operands) >= 3: + a = _rng(inst.operands[1], self.ranges) + b = _rng(inst.operands[2], self.ranges) + if a is None or b is None: + return None + return _union(a, b) + if op == Op.NOT and inst.operands: + r = _rng(inst.operands[0], self.ranges) + if r is None: + return None + if r == (0, 0): + return (1, 1) + if r == (1, 1): + return (0, 0) + return (0, 1) + if op == Op.NEG and inst.operands: + r = _rng(inst.operands[0], self.ranges) + return (-r[1], -r[0]) if r else None + if op == Op.ABS and inst.operands: + r = _rng(inst.operands[0], self.ranges) + if r is None: + return None + if r[0] >= 0: + return r + if r[1] <= 0: + return (-r[1], -r[0]) + return (0, max(-r[0], r[1])) + if op == Op.TO_BOOL: + return (0, 1) + if op in (Op.EQ, Op.NE, Op.LT, Op.LE, Op.GT, Op.GE) and len(inst.operands) >= 2: + a = _rng(inst.operands[0], self.ranges) + b = _rng(inst.operands[1], self.ranges) + if a is None or b is None: + return (0, 1) + return _compare_range(op, a, b) + if op in (Op.ADD, Op.SUB, Op.MUL, Op.DIV, Op.MOD, Op.MIN, Op.MAX) and len( + inst.operands + ) >= 2: + a = _rng(inst.operands[0], self.ranges) + b = _rng(inst.operands[1], self.ranges) + if a is None or b is None: + return None + return _arith_range(op, a, b) + if op in (Op.AND, Op.OR) and len(inst.operands) >= 2: + a = _rng(inst.operands[0], self.ranges) + b = _rng(inst.operands[1], self.ranges) + if a is None or b is None: + return (0, 1) + av = None if a[0] != a[1] else bool(a[0]) + bv = None if b[0] != b[1] else bool(b[0]) + if op == Op.AND: + if av is False or bv is False: + return (0, 0) + if av is True and bv is True: + return (1, 1) + else: + if av is True or bv is True: + return (1, 1) + if av is False and bv is False: + return (0, 0) + return (0, 1) + return None + + def _feasible_successors(self, block: BasicBlock) -> list[BasicBlock]: + term = block.terminator + if term is None: + return list(block.successors) + if term.op == Op.BRANCH and term.operands and len(term.target_blocks) >= 2: + cond = _rng(term.operands[0], self.ranges) + if cond == (1, 1): + return [term.target_blocks[0]] + if cond == (0, 0): + return [term.target_blocks[1]] + return list(block.successors) + + +def infer_loop_ranges(func: Function) -> dict[int, Range]: + """Infer induction-variable bounds for natural loops. + + These bounds are only used to derive per-call argument ranges; they are not + used to fold the loop's own controlling branch. + """ + from optimizer.analysis import FunctionAnalysis + + result: dict[int, Range] = {} + try: + analysis = FunctionAnalysis(func) + except Exception: + return result + + for loop in analysis.loops.loops: + header = loop.header + for phi in header.phi_nodes: + if len(phi.operands) != 2 or len(phi.target_blocks) != 2 or phi.result is None: + continue + inside_idx = None + outside_idx = None + for i, pred in enumerate(phi.target_blocks): + if pred in loop.blocks: + inside_idx = i + else: + outside_idx = i + if inside_idx is None or outside_idx is None: + continue + init = _rng(phi.operands[outside_idx], {}) + if init is None: + continue + back = phi.operands[inside_idx] + step = None + if isinstance(back, Value) and back.defining_inst is not None: + bi = back.defining_inst + if bi.op in (Op.ADD, Op.SUB) and len(bi.operands) >= 2: + if bi.op == Op.ADD: + pairs = ( + (bi.operands[0], bi.operands[1]), + (bi.operands[1], bi.operands[0]), + ) + else: + # SUB is not commutative: only phi - const is an + # induction step, never const - phi. + pairs = ((bi.operands[0], bi.operands[1]),) + for a, b in pairs: + if a is phi.result: + step_const = _get_const(b) + if step_const is not None: + step = int(step_const) * (1 if bi.op == Op.ADD else -1) + break + if not step: + continue + + for block in loop.blocks: + term = block.terminator + if term is None or term.op != Op.BRANCH or len(term.target_blocks) < 2: + continue + cond = term.operands[0] if term.operands else None + if not isinstance(cond, Value) or cond.defining_inst is None: + continue + ci = cond.defining_inst + if ci.op not in (Op.LT, Op.LE, Op.GT, Op.GE) or len(ci.operands) < 2: + continue + left, right = ci.operands[0], ci.operands[1] + op = ci.op + if left is phi.result and _get_const(right) is not None: + bound = int(_get_const(right)) + elif right is phi.result and _get_const(left) is not None: + bound = int(_get_const(left)) + # Flip the comparison direction so the phi is on the left. + op = { + Op.LT: Op.GT, + Op.LE: Op.GE, + Op.GT: Op.LT, + Op.GE: Op.LE, + }[ci.op] + else: + continue + true_inside = term.target_blocks[0] in loop.blocks + false_inside = term.target_blocks[1] in loop.blocks + inside_op = op if true_inside else None + if inside_op is None and false_inside: + # Invert the comparison for the false edge. + inside_op = { + Op.LT: Op.GE, + Op.LE: Op.GT, + Op.GT: Op.LE, + Op.GE: Op.LT, + }.get(op) + if inside_op is None: + continue + + lo, hi = init + if step > 0: + # The induction variable starts at init and moves toward the + # loop bound; init's upper part is not a loop-body bound. + if inside_op == Op.LT: + hi = bound - 1 + elif inside_op == Op.LE: + hi = bound + elif inside_op == Op.GT: + lo = max(lo, bound + 1) + elif inside_op == Op.GE: + lo = max(lo, bound) + else: + if inside_op == Op.GT: + lo = max(lo, bound + 1) + elif inside_op == Op.GE: + lo = max(lo, bound) + elif inside_op == Op.LT: + hi = bound - 1 + elif inside_op == Op.LE: + hi = bound + if lo <= hi: + result[phi.result.id] = (lo, hi) + break + + return result + + +def _get_const(value) -> int | None: + if isinstance(value, Constant): + return int(value.value) if isinstance(value.value, int) else None + if isinstance(value, Value) and value.defining_inst is not None: + inst = value.defining_inst + if inst.op == Op.CONST_INT: + return int(inst.imm_int or 0) + if inst.op == Op.CONST_BOOL: + return 1 if inst.imm_int else 0 + return None + + +def range_fold_branches( + func: Function, + param_ranges: dict[int, Range] | None = None, + config: OptimizerConfig | None = None, +) -> bool: + """Fold branches provable from ranges and mark safe division checks.""" + analysis = RangeAnalysis(func, param_ranges, config) + ranges = analysis.run() + changed = False + + for block in func.blocks: + for inst in block.instructions: + if inst.op in (Op.DIV, Op.MOD) and len(inst.operands) >= 2: + divisor = _rng(inst.operands[1], ranges) + if divisor is not None and (divisor[0] > 0 or divisor[1] < 0): + if not getattr(inst, "no_zero_check", False): + inst.no_zero_check = True + changed = True + + term = block.terminator + if ( + term is not None + and term.op == Op.BRANCH + and term.operands + and len(term.target_blocks) >= 2 + ): + # Only use ranges from this run. Persistent value_range hints may + # include loop-body induction bounds, which are valid inside an + # iteration but must never fold the loop's own exit branch. + cond = _rng(term.operands[0], ranges) + if cond not in ((0, 0), (1, 1)): + continue + target = term.target_blocks[0] if cond == (1, 1) else term.target_blocks[1] + dead = term.target_blocks[1] if cond == (1, 1) else term.target_blocks[0] + term.op = Op.JUMP + term.remove_from_uses() + term.operands = [] + term.target_blocks = [target] + term.imm_str = target.label + block.successors = [target] + if block in dead.predecessors: + dead.predecessors.remove(block) + changed = True + return changed + + +def _merge_param_range(old: Range | None, new: Range | None) -> Range | None: + return _union(old, new) + + +def analyze_module_ranges( + module: Module, config: OptimizerConfig | None = None +) -> dict[str, dict[int, Range]]: + """Propagate call-site ranges into callee parameter ranges. + + Returns the final range map for every function. Call instructions receive + `arg_ranges` so specialization can choose a call-specific version. + """ + cfg = config or OptimizerConfig.from_env() + funcs = {f.name: f for f in module.functions} + param_ranges: dict[str, list[Range | None]] = { + name: [None] * len(func.params) for name, func in funcs.items() + } + param_unknown: set[tuple[str, int]] = set() + + # Loop-local facts are only valid for calls executed inside that loop. + from optimizer.analysis import FunctionAnalysis + block_loop: dict[str, dict[int, set[int]]] = {} + for name, func in funcs.items(): + try: + loops = FunctionAnalysis(func).loops + except Exception: + block_loop[name] = {} + continue + mapping: dict[int, set[int]] = {} + for loop in loops.loops: + block_ids = {id(b) for b in loop.blocks} + for b in loop.blocks: + mapping[id(b)] = block_ids + block_loop[name] = mapping + + def _range_for_arg(arg, owner: str | None, call: Instruction, r): + if r is None or not isinstance(arg, Value): + return r + def_block = arg.defining_inst.block if arg.defining_inst else None + if def_block is None or call.block is None or owner is None: + return r + loop_blocks = block_loop.get(owner, {}).get(id(def_block)) + if loop_blocks is not None and id(call.block) not in loop_blocks: + return None + return r + + def merge_call(call: Instruction, analyses: dict[str, dict[int, Range]] | None): + target = funcs.get(call.imm_str or "") + if target is None: + return False + changed = False + arg_ranges: list[Range | None] = [] + owner = call.block.func.name if call.block and call.block.func else None + owner_ranges = (analyses or {}).get(owner or "", {}) + for i, arg in enumerate(call.operands): + r = _rng(arg, owner_ranges) + if r is None and isinstance(arg, Value): + r = arg.value_range + r = _range_for_arg(arg, owner, call, r) + arg_ranges.append(r) + if i < len(param_ranges[target.name]): + if r is None: + key = (target.name, i) + if key not in param_unknown: + param_unknown.add(key) + param_ranges[target.name][i] = None + changed = True + elif (target.name, i) not in param_unknown: + old = param_ranges[target.name][i] + new = _merge_param_range(old, r) + if new != old: + param_ranges[target.name][i] = new + changed = True + call.arg_ranges = arg_ranges + return changed + + # Seed with constants visible at call sites. + for func in module.functions: + for inst in func.all_instructions(): + if inst.op == Op.CALL: + merge_call(inst, None) + + last_analyses: dict[str, dict[int, Range]] = {} + for _ in range(cfg.max_range_iterations): + analyses: dict[str, dict[int, Range]] = {} + for name, func in funcs.items(): + seeded = { + p.id: param_ranges[name][i] + for i, p in enumerate(func.params) + if i < len(param_ranges[name]) and param_ranges[name][i] is not None + } + func.param_ranges = seeded + fixed = infer_loop_ranges(func) + analyses[name] = RangeAnalysis(func, seeded, cfg, fixed).run() + + changed = False + for func in module.functions: + for inst in func.all_instructions(): + if inst.op == Op.CALL: + changed |= merge_call(inst, analyses) + last_analyses = analyses + if not changed: + break + + # Final pass records arg_ranges with the freshest analysis. + if last_analyses: + for func in module.functions: + for inst in func.all_instructions(): + if inst.op == Op.CALL: + merge_call(inst, last_analyses) + return last_analyses diff --git a/src/optimizer/types.py b/src/optimizer/types.py index f4127d8..6698a60 100644 --- a/src/optimizer/types.py +++ b/src/optimizer/types.py @@ -281,6 +281,10 @@ def _analyze_function(self, func: Function): op_val is not inst.operands[0] ): # it's the value, not the target new_state = self.FULL_ESCAPE + elif inst.op in (Op.PTR_STORE, Op.FIELD_ADDR): + # The address of the struct (or one of its fields) may + # outlive this frame; be conservative. + new_state = self.FULL_ESCAPE elif inst.op == Op.PHI: # Phi merges: if any incoming value escapes, the phi result escapes # and the phi result should propagate back to sources @@ -298,5 +302,10 @@ def can_sroa(self, value: Value) -> bool: return self.get_escape(value) == self.NO_ESCAPE def can_stack_alloc(self, value: Value) -> bool: - """Can this allocation be placed on the stack?""" - return self.get_escape(value) <= self.ARG_ESCAPE + """Can this allocation be placed on the stack? + + ARG_ESCAPE only says the value reaches a call. The callee may store + the pointer in a global/list/field, so without a callee-summary this + case must stay heap-allocated. + """ + return self.get_escape(value) == self.NO_ESCAPE diff --git a/src/parser.py b/src/parser.py index a8c94d5..98384fa 100644 --- a/src/parser.py +++ b/src/parser.py @@ -75,6 +75,9 @@ def _is_large_int(value: Any) -> bool: # Parse-time evaluation timeout (seconds) eval_timeout_seconds: float = 0.5 +# Variables whose address is taken must never be constant-propagated. +_address_taken_vars: set[str] = set() + def _parse_c_signatures( c_file: str, @@ -571,6 +574,32 @@ def _split_args(args_text: str) -> list[str]: return [arg.strip() for arg in args if arg.strip()] +def _canonical_pointer_type(token: str) -> tuple[str, int] | None: + """Split a type token into (base, pointer_depth). + + Accepted spellings: ``int*``, ``*int``, ``**int`` and ``int**``. + Plain (non-pointer) types return None. This deliberately does not treat + ``int *name`` as a pointer because that spelling is reserved for varargs. + """ + if not token or "*" not in token: + return None + base = token + depth = 0 + while base.startswith("*"): + depth += 1 + base = base[1:] + while base.endswith("*"): + depth += 1 + base = base[:-1] + if not base or base.endswith("*") or depth == 0: + return None + return base, depth + + +def _pointer_type_string(base: str, depth: int) -> str: + return "*" * depth + base + + def _parse_typed_arg( arg: str, check_comma: bool, stream: InputStream, line: int ) -> tuple[str, str | None]: @@ -581,6 +610,13 @@ def _parse_typed_arg( """ import re + # Pointer parameter: ``int* p`` / ``*int p`` / ``int** p``. + pointer_match = re.match(r"^(\*{1,2})(\w+)\s+(\w+)$", arg) + if pointer_match and pointer_match[2] in types: + depth = len(pointer_match[1]) + return (pointer_match[3], _pointer_type_string(pointer_match[2], depth)) + + # Varargs: ``int *name`` / ``int **name`` (space before the stars). if varargs_match := re.match(r"^(\w+)\s+(\*\*?)(\w+)$", arg): arg_type = varargs_match[1] if arg_type in types: @@ -594,6 +630,10 @@ def _parse_typed_arg( if len(parts) == 1: # Untyped argument: "x" return (parts[0], None) + elif len(parts) == 2 and _canonical_pointer_type(parts[0]): + base, depth = _canonical_pointer_type(parts[0]) # type: ignore[misc] + if base in types or base in {"string", "str", "float", "f64", "bool", "void"}: + return (parts[1], _pointer_type_string(base, depth)) elif len(parts) == 2 and parts[0] in types: # Typed argument: "int x" return (parts[1], parts[0]) @@ -726,6 +766,25 @@ def _normalize_operators(text: str) -> str: def _ast_to_dict(node): """Convert AST node to dictionary representation.""" + if isinstance(node, ast.Name) and node.id in ("true", "false"): + return {"type": "bool", "value": node.id} + + if isinstance(node, ast.Constant): + value = node.value + if isinstance(value, bool): + return {"type": "bool", "value": value} + if isinstance(value, str): + return {"type": "string", "value": value} + if isinstance(value, bytes): + return { + "type": "bytes", + "value": value.decode("utf-8", errors="replace"), + } + if isinstance(value, int): + return {"type": "int", "value": value} + if isinstance(value, float): + return {"type": "float", "value": value} + if not node._fields: return str(get_type(node)) @@ -744,6 +803,19 @@ def _ast_to_dict(node): # For now, treat non-empty dicts as sets (will fail with proper error later) # This shouldn't happen often since Python dict syntax requires keys + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id in ("__addr__", "__deref__") + and len(node.args) == 1 + and not node.keywords + ): + kind = "addr" if node.func.id == "__addr__" else "deref" + value = _ast_to_dict(node.args[0]) + if kind == "addr" and isinstance(value, dict) and set(value.keys()) == {"id"}: + _address_taken_vars.add(value["id"]) + return {"type": kind, "value": value} + # Check if this is a method call on a variable (x.method(args)) # We want to convert it to method(x, args) if method is a builtin function if ( @@ -1090,6 +1162,148 @@ def _contains_variable_refs(node): return False +def _find_pointer_operand_end(text: str, start: int) -> int: + """Return the end index of the primary/postfix expression at *start*.""" + n = len(text) + i = start + while i < n and text[i].isspace(): + i += 1 + if i >= n: + return i + + def skip_balanced(j: int, open_ch: str, close_ch: str) -> int: + depth = 0 + in_str = None + while j < n: + ch = text[j] + if in_str: + if ch == "\\": + j += 2 + continue + if ch == in_str: + in_str = None + elif ch in ("'", '"'): + in_str = ch + elif ch == open_ch: + depth += 1 + elif ch == close_ch: + depth -= 1 + if depth == 0: + return j + 1 + j += 1 + return n + + if text[i] == "(": + return skip_balanced(i, "(", ")") + if text[i] in ("'", '"'): + quote = text[i] + j = i + 1 + while j < n: + if text[j] == "\\": + j += 2 + continue + if text[j] == quote: + return j + 1 + j += 1 + return n + if text[i].isdigit() or text[i] == ".": + j = i + while j < n and (text[j].isalnum() or text[j] in "._"): + j += 1 + return j + if text[i].isalpha() or text[i] == "_": + j = i + 1 + while j < n and (text[j].isalnum() or text[j] == "_"): + j += 1 + # Postfix chain: .field, [index], (call) + while j < n: + if text[j] == "." and j + 1 < n and (text[j + 1].isalpha() or text[j + 1] == "_"): + j += 2 + while j < n and (text[j].isalnum() or text[j] == "_"): + j += 1 + elif text[j] == "[": + j = skip_balanced(j, "[", "]") + elif text[j] == "(": + j = skip_balanced(j, "(", ")") + else: + break + return j + return i + 1 + + +def _rewrite_pointer_syntax(text: str) -> str: + """Translate unary ``&expr`` and ``*expr`` into internal pseudo-calls. + + This runs before Python's AST parser sees the expression. It only treats + ``&``/``*`` as unary in prefix position, so multiplication and the logical + operators continue to work exactly as before. + """ + unary_prefix = set("([{,;:=+-*/%<>!&|^~?") + out: list[str] = [] + i = 0 + prev_sig = "" + n = len(text) + while i < n: + ch = text[i] + if ch in ("'", '"'): + quote = ch + j = i + 1 + while j < n: + if text[j] == "\\": + j += 2 + continue + if text[j] == quote: + j += 1 + break + j += 1 + out.append(text[i:j]) + prev_sig = quote + i = j + continue + + unary = (prev_sig == "" or prev_sig in unary_prefix) + + if ( + ch == "&" + and unary + and (i + 1 >= n or text[i + 1] != "&") + and (i == 0 or text[i - 1:i] != "&") + ): + start = i + 1 + end = _find_pointer_operand_end(text, start) + if end > start: + out.append("__addr__(" + text[start:end].strip() + ")") + prev_sig = ")" + i = end + continue + + if ch == "*" and unary and (i == 0 or text[i - 1] != "*"): + if i + 1 < n and text[i + 1] == "*": + start = i + 2 + end = _find_pointer_operand_end(text, start) + if end > start: + out.append( + "__deref__(__deref__(" + text[start:end].strip() + "))" + ) + prev_sig = ")" + i = end + continue + else: + start = i + 1 + end = _find_pointer_operand_end(text, start) + if end > start: + out.append("__deref__(" + text[start:end].strip() + ")") + prev_sig = ")" + i = end + continue + + out.append(ch) + if not ch.isspace(): + prev_sig = ch + i += 1 + return "".join(out) + + def parse_expr(text: str): """Parse an expression using Python's AST parser and optionally evaluate constants.""" from runtime import eval_expr # type: ignore @@ -1111,6 +1325,7 @@ def parse_expr(text: str): if text_stripped.startswith('b"') and text_stripped.endswith('"'): return parse_literal(text_stripped) + text = _rewrite_pointer_syntax(text) text = _normalize_operators(text) try: expr = ast.parse(text, mode="eval").body @@ -1180,6 +1395,12 @@ def parse_expr(text: str): ): # Variable reference - keep as expr_dict if _is_large_int(evaluated): return expr_dict + if isinstance(evaluated, bytes): + # Bytecode represents bytes as an escaped string payload. + return { + "type": "bytes", + "value": evaluated.decode("utf-8", errors="replace"), + } return evaluated except Exception: @@ -1326,6 +1547,7 @@ def parse_var( Returns: Variable AST node or SkipNode if const """ + is_declaration = var_type is not None stream.strip() if not stream.consume("="): @@ -1363,6 +1585,7 @@ def parse_var( node = make_node( stream, type="var", name=name, value_type=var_type, value=value, mods=mods ) + node["declaration"] = is_declaration if list_capacity is not None or list_elem_type is not None: node["list_capacity"] = list_capacity node["list_elem_type"] = list_elem_type @@ -1378,6 +1601,10 @@ def _is_runtime_expression(value: dict) -> bool: if "slice" in value: return True + # Pointer operations are runtime expressions. + if value.get("type") in ("addr", "deref"): + return True + # Field access: obj.field if "attr" in value: return True @@ -1431,6 +1658,10 @@ def cast_value(value: Any, required_type: str): if _is_runtime_expression(value): return value + # Pointer-typed destinations always retain the runtime expression. + if isinstance(required_type, str) and required_type.startswith("*"): + return value + # Accept any type if required type is 'any' if required_type == "any": return value @@ -2171,6 +2402,13 @@ def parse_any( pass field_name = stream.consume_word() + pointer = _canonical_pointer_type(field_type) + if pointer: + base, depth = pointer + field_type = _pointer_type_string(base, depth) + elif field_name.startswith("*") and not field_name.startswith("**"): + field_name = field_name[1:] + field_type = _pointer_type_string(field_type, 1) fields.append({"name": field_name, "type": field_type}) @@ -2445,6 +2683,19 @@ def parse_any( type = stream.consume_word() name = stream.consume_word() + + # Pointer declarations: ``int* p``, ``*int p``, ``int** p`` and the + # spaced form ``int *p``. + pointer = _canonical_pointer_type(type) + if name.startswith("*") and not name.startswith("**") and type in types: + name = name[1:] + pointer = (type, 1) + elif name.startswith("**") and type in types: + name = name[2:] + pointer = (type, 2) + if pointer: + base, depth = pointer + type = _pointer_type_string(base, depth) list_capacity = None list_elem_type = None @@ -2597,17 +2848,82 @@ def parse_any( stream.format_error(f"Unknown directive: #{directive}") ) + # Statements may begin with a parenthesized lvalue, e.g. (*p).x = 1. + if stream.peek(1) == "(": + def consume_group() -> str: + depth = 0 + out = "" + while stream.text: + ch = stream.peek(1) + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + out += ch + stream.consume(ch) + if depth == 0: + break + return out + + stmt_start = stream.line + target_text = consume_group() + stream.strip() + if stream.peek(1) == ".": + stream.consume(".") + stream.strip() + field_name = stream.consume_word() + stream.strip() + if stream.peek(1) == "=" and stream.peek(2) != "=": + stream.consume("=") + stream.strip() + value = _consume_expression(stream).strip().rstrip(";") + node = make_node( + stream, + type="field_assign", + target=parse_expr(target_text[1:-1]), + field=field_name, + value=parse_expr(value), + ) + node["line"] = stmt_start + return node + raise SyntaxError( + stream.format_error("Unexpected parenthesized statement") + ) + word = stream.consume_word() # Ignore if empty (comment or blank line) - consume_word() already advanced the stream if not word.strip(): return None + # Pointer store: *p = value (or **pp = value). + if word.startswith("*"): + depth = len(word) - len(word.lstrip("*")) + target_text = word[depth:].strip() + stream.strip() + if target_text and stream.peek(1) == "=" and stream.peek(2) != "=": + stream.consume("=") + stream.strip() + value = _consume_expression(stream).strip().rstrip(";") + target = parse_expr(target_text) + for _ in range(depth): + target = {"type": "deref", "value": target} + node = make_node( + stream, type="ptr_assign", target=target, value=parse_expr(value) + ) + node["line"] = stmt_line + return node + # Field assignment: obj.field = value (word is "obj.field") - if "." in word and stream.peek(1) == "=": - parts = word.split(".", 1) + if "." in word and stream.peek(1) == "=" and stream.peek(2) != "=": + parts = word.rsplit(".", 1) if len(parts) == 2: - target, field = parts + target_text, field = parts + target = ( + target_text + if target_text.isidentifier() + else parse_expr(target_text) + ) stream.consume("=") stream.strip() value = _consume_expression(stream).strip().rstrip(";") @@ -2821,10 +3137,18 @@ def parse_any( value=binary_expr, mods=[], ) + node["declaration"] = False node["line"] = stmt_line return node - elif word in types: + elif word in types or ( + _canonical_pointer_type(word) + and _canonical_pointer_type(word)[0] in types # type: ignore[index] + ): + pointer = _canonical_pointer_type(word) + if pointer: + base, depth = pointer + word = _pointer_type_string(base, depth) name = stream.consume_word() list_capacity = None list_elem_type = None @@ -2911,6 +3235,19 @@ def parse_any( ) stream.consume("else") + stream.strip() + # Accept both ``elif`` and the common ``else if`` spelling. + if stream.peek_word() == "if": + stream.consume("if") + args = parse_args(stream, check_comma=False) + arg_value = args[0][0] if args else "" + condition = parse_expr(arg_value) + scope = parse_scope(stream, level + 1) + node["elifs"].append( + {"type": "elif", "condition": condition, "scope": scope} + ) + continue + scope = parse_scope(stream, level + 1) node["else"] = scope @@ -3130,7 +3467,7 @@ def parse_any( return { "type": "for_in", "var": varname, - "iterable": parse_literal(iterable_expr), + "iterable": parse_expr(iterable_expr), "scope": scope, "line": for_line, } @@ -3365,6 +3702,7 @@ def parse(text: str | InputStream, level: int = 0, file: str = "") -> AstType: _imported_files.clear() # Reset import tracking for each top-level parse disable_eval = False # Reset pragma state for each top-level parse eval_timeout_seconds = 0.5 + _address_taken_vars.clear() _eval_cache.clear() _eval_failed_cache.clear() _eval_timeout_funcs.clear() @@ -3514,12 +3852,23 @@ def _replace_const_refs( node: Any, const_map: dict[str, Any], skip_name: str | None = None ) -> Any: if isinstance(node, dict): + # Never substitute inside address-of/dereference targets: + # their identity is the whole point of a pointer. + if node.get("type") in ("addr", "deref"): + return node + if node.get("type") == "ptr_assign": + result = dict(node) + result["value"] = _replace_const_refs( + node.get("value"), const_map, skip_name + ) + return result if set(node.keys()) == {"id"}: name = node.get("id") if ( isinstance(name, str) and name != skip_name and name in const_map + and name not in _address_taken_vars ): return copy.deepcopy(const_map[name]) @@ -3639,7 +3988,7 @@ def walk_assignments(node: Any): local_inline: dict[str, Any] = {} for name, count in assign_counts.items(): - if count == 1: + if count == 1 and name not in _address_taken_vars: value = assign_values.get(name) if _is_inlineable_const_value(value): local_inline[name] = _normalize_literal_value(value) diff --git a/src/run_single_test.py b/src/run_single_test.py index a01c28c..0b4b865 100644 --- a/src/run_single_test.py +++ b/src/run_single_test.py @@ -62,8 +62,8 @@ def ensure_runtime_object(): except OSError: pass - tmp_rt_obj = RUNTIME_OBJ.with_suffix(".o.tmp") - tmp_sc_obj = SYSCALL_OBJ.with_suffix(".o.tmp") + tmp_rt_obj = RUNTIME_OBJ.with_suffix(f".{os.getpid()}.o.tmp") + tmp_sc_obj = SYSCALL_OBJ.with_suffix(f".{os.getpid()}.o.tmp") rt_cmd = [ "gcc", @@ -107,8 +107,20 @@ def ensure_runtime_object(): pass raise RuntimeError(result.stderr.strip() or "Failed to assemble syscall.S") - tmp_rt_obj.replace(RUNTIME_OBJ) - tmp_sc_obj.replace(SYSCALL_OBJ) + try: + tmp_rt_obj.replace(RUNTIME_OBJ) + except OSError: + try: + tmp_rt_obj.unlink() + except OSError: + pass + try: + tmp_sc_obj.replace(SYSCALL_OBJ) + except OSError: + try: + tmp_sc_obj.unlink() + except OSError: + pass return str(RUNTIME_OBJ), str(SYSCALL_OBJ) diff --git a/src/runtime.py b/src/runtime.py index 92a3cec..a818573 100644 --- a/src/runtime.py +++ b/src/runtime.py @@ -1,6 +1,7 @@ from builtin_funcs import funcs from parser import parse_expr, get_type, vars from typing import Any, cast +import os import sys import threading @@ -9,6 +10,30 @@ # Track the actual current vars (updated when entering/exiting functions) _current_runtime_vars = vars +_global_vars: dict = {} +_function_locals: set[str] = set() + + +def _collect_local_decls(node, out: set[str]) -> None: + if isinstance(node, dict): + if node.get("type") == "function": + return + if node.get("type") == "var" and node.get("name") and node.get("declaration"): + out.add(node["name"]) + for value in node.values(): + _collect_local_decls(value, out) + elif isinstance(node, list): + for item in node: + _collect_local_decls(item, out) + + +def _lookup_var_cell(name: str): + """Look up a variable cell in the current scope, then globals.""" + if name in vars: + return vars[name] + if name in _global_vars: + return _global_vars[name] + return None # Runtime context for error formatting _runtime_file: str | None = None @@ -147,6 +172,14 @@ def __init__(self, level=1): super().__init__() +class ReturnSignal(Exception): + """Internal control-flow signal used to unwind a function cleanly.""" + + def __init__(self, value): + self.value = value + super().__init__() + + # Helper functions def is_struct_def(obj: Any) -> bool: """Check if an object is a struct definition""" @@ -199,6 +232,8 @@ def _process_function_arg(arg: Any, old_vars: dict) -> Any: # Field access elif "attr" in arg: return eval_expr_node(arg) + elif arg.get("type") in ("addr", "deref", "bytes"): + return eval_expr_node(arg) # Regular dict with value else: return arg.get("value", eval_expr(arg)) @@ -235,7 +270,9 @@ def run_func( from parser import cast_args global vars, runtime, _current_runtime_vars, _runtime_current_func, _runtime_current_line + global _function_locals old_vars = vars + old_function_locals = _function_locals old_func = _runtime_current_func old_line = _runtime_current_line # Save calling line @@ -266,6 +303,9 @@ def run_func( vars = _preserve_struct_definitions() _current_runtime_vars = vars # Update current vars for debugger _runtime_current_func = name # Track function for error reporting + declared: set[str] = set() + _collect_local_decls(func.get("func"), declared) + _function_locals = declared # Bind function arguments to the scope if isinstance(func.get("args"), list): @@ -287,9 +327,13 @@ def run_func( if not all(isinstance(item, dict) for item in func_body): raise RuntimeError("Invalid function body.") - out = run_scope(cast(AstType, func_body), level + 1) + try: + out = run_scope(cast(AstType, func_body), level + 1) + except ReturnSignal as signal: + out = signal.value vars = old_vars + _function_locals = old_function_locals if not is_builtin: _current_runtime_vars = vars # Only restore if we changed it _runtime_current_func = old_func # Restore function name @@ -307,8 +351,12 @@ class CompiletimeEvalException(Exception): ... def _get_indexed_value(target: Any, index: Any) -> Any: - """Get value from target at index, handling various types""" - # Ensure index is numeric + """Get value from target at index, handling lists and dictionaries.""" + if isinstance(target, dict): + if index in target: + return target[index] + raise IndexError(f"Key error: {index!r}") + # Ensure index is numeric for array-like targets if not isinstance(index, int): index = ensure_int(index, "Index") @@ -357,7 +405,7 @@ def _get_indexed_value(target: Any, index: Any) -> Any: error_msg = f"Index error: list index out of range: {index}" if isinstance(target, list): error_msg += f" (length: {len(target)})" - raise RuntimeError(error_msg) from e + raise IndexError(error_msg) from e raise RuntimeError(f"Cannot index into type {type(target).__name__}") @@ -377,8 +425,9 @@ def _handle_list_indexing(node: dict) -> Any: index = extract_value(index) # Handle variable references - if isinstance(target, str) and target in vars: - target_val = vars[target].get("value") + if isinstance(target, str) and (target in vars or target in _global_vars): + cell = vars[target] if target in vars else _global_vars[target] + target_val = cell.get("value") return _get_indexed_value(target_val, index) return _get_indexed_value(target, index) @@ -390,8 +439,11 @@ def _handle_field_access(node: dict) -> Any: field_name = node["attr"] # If target is a variable name, look it up - if isinstance(target, str) and target in vars: - obj = vars[target].get("value") + if isinstance(target, str) and (target in vars or target in _global_vars): + cell = vars[target] if target in vars else _global_vars[target] + obj = cell.get("value") + if isinstance(obj, Pointer): + obj = obj.get() # Check if it's a Python object if hasattr(obj, "__dict__") or hasattr(obj, field_name): @@ -417,6 +469,9 @@ def _handle_field_access(node: dict) -> Any: f"Python object has no attribute '{field_name}'" ) from exc + if isinstance(target, Pointer): + target = target.get() + # If target is already an object (dict), access the field if isinstance(target, dict) and field_name in target: return target[field_name] @@ -462,12 +517,102 @@ def _handle_unary_op(node: dict) -> Any: return operand +class Pointer: + """A stable reference to a value cell in the Python runtime.""" + + __slots__ = ("_get", "_set", "pointee_type") + + def __init__(self, getter, setter, pointee_type=None): + self._get = getter + self._set = setter + self.pointee_type = pointee_type + + def get(self): + return self._get() + + def set(self, value): + self._set(value) + + def __repr__(self): + return f"" + + +def _make_pointer(node) -> Pointer: + if not isinstance(node, dict): + raise RuntimeError("Cannot take the address of this expression") + if "id" in node and "func" not in node: + name = node["id"] + cell = _lookup_var_cell(name) + if cell is None: + raise RuntimeError(f"Variable '{name}' is not defined") + + def getter(): + return cell.get("value") + + def setter(value): + cell["value"] = value + + return Pointer(getter, setter, cell.get("type")) + if "attr" in node and "value" in node: + obj = eval_expr_node(node["value"]) + field = node["attr"] + if isinstance(obj, dict): + def getter(): + return obj.get(field) + + def setter(value): + obj[field] = value + + return Pointer(getter, setter) + raise RuntimeError(f"Cannot take the address of field '{field}'") + if "slice" in node and "value" in node: + container = eval_expr_node(node["value"]) + index = eval_expr_node(node["slice"]) + if not isinstance(container, list): + raise RuntimeError("Cannot take the address of a non-list element") + index = ensure_int(index, "Index") + + def getter(): + return container[index] + + def setter(value): + container[index] = value + + return Pointer(getter, setter) + raise RuntimeError("Cannot take the address of this expression") + + def eval_expr_node(node) -> int | float | bool | str | None | Any: from parser import get_type if not isinstance(node, dict): return node + if node.get("type") == "bool": + raw = node.get("value") + if isinstance(raw, str): + return raw in ("true", "True", "1") + return bool(raw) + + if node.get("type") == "bytes": + raw = node.get("value") + if isinstance(raw, bytes): + return raw + if isinstance(raw, str): + return raw.encode("latin-1") + return b"" + + if node.get("type") == "addr": + return _make_pointer(node.get("value")) + + if node.get("type") == "deref": + pointer = eval_expr_node(node.get("value")) + if isinstance(pointer, Pointer): + return pointer.get() + if isinstance(pointer, int) and pointer == 0: + raise RuntimeError("Null pointer dereference") + raise RuntimeError("Cannot dereference a non-pointer value") + # Unary operations if "op" in node and node["op"] in ("USub", "UAdd", "Not"): return _handle_unary_op(node) @@ -487,11 +632,12 @@ def eval_expr_node(node) -> int | float | bool | str | None | Any: # Variable reference if "id" in node: value = node["id"] - if value in vars: - var_value = vars[value].get("value") + cell = _lookup_var_cell(value) + if cell is not None: + var_value = cell.get("value") # Return the actual value, even if it's None # Only return the variable name if the key doesn't exist - return var_value if "value" in vars[value] else value + return var_value if "value" in cell else value # At parse time, if variable not found, return the node unchanged # This prevents treating undefined variables as string literals return value if runtime else node @@ -825,12 +971,13 @@ def _execute_node_var(node: dict): # Evaluate f-strings and expressions if isinstance(value, dict): # Check if it's a set/list literal that needs evaluation - if value.get("type") in ("set", "list"): + if value.get("type") in ("set", "list", "bool", "bytes"): value = eval_expr_node(value) # Check if it's a runtime expression (has slice, attr, or other runtime features) elif ( "slice" in value or "attr" in value + or value.get("type") in ("addr", "deref") or ("values" in value and "value" not in value) ): value = eval_expr_node(value) @@ -839,11 +986,40 @@ def _execute_node_var(node: dict): else: value = value["value"] - # Make a deep copy of mutable objects to avoid mutating the AST - if isinstance(value, (list, dict, set)): - value = copy.deepcopy(value) - - vars[node["name"]] = {"type": node["value_type"], "value": value} + if node.get("value_type") == "dict" and isinstance(value, (set, list)) and not value: + value = {} + + # Static lists are fixed-size and pre-filled with type-appropriate + # zero values: list name[capacity:type] = [] + capacity = node.get("list_capacity") + if capacity is not None and isinstance(value, list) and not value: + elem_type = node.get("list_elem_type") + if elem_type in ("str", "string"): + default = "" + elif elem_type in ("float", "f64"): + default = 0.0 + elif elem_type == "bool": + default = False + else: + default = 0 + value = [copy.deepcopy(default) for _ in range(int(capacity))] + + # Assignments alias mutable objects, matching the native/C reference + # semantics. Fresh literals and struct constructors already produce a new + # object for every evaluation, so no defensive copy is needed here. + name = node["name"] + if name in vars: + # Mutate the existing cell so pointers obtained with &name keep + # aliasing the variable after reassignment. + vars[name]["type"] = node["value_type"] + vars[name]["value"] = value + elif name in _function_locals: + vars[name] = {"type": node["value_type"], "value": value} + elif name in _global_vars: + _global_vars[name]["type"] = node["value_type"] + _global_vars[name]["value"] = value + else: + vars[name] = {"type": node["value_type"], "value": value} def _execute_node_index_assign(node: dict): @@ -852,10 +1028,21 @@ def _execute_node_index_assign(node: dict): if target_name not in vars: raise RuntimeError(f"Variable '{target_name}' is not defined") - target = vars[target_name]["value"] - index = ensure_int(eval_expr(node["index"]), "Index") + cell = _lookup_var_cell(target_name) + if cell is None: + raise RuntimeError(f"Variable '{target_name}' is not defined") + target = cell["value"] + index = eval_expr(node["index"]) value = eval_expr(node["value"]) + if isinstance(target, dict): + target[index] = value + return + if isinstance(index, str): + index = ensure_int(index, "Index") + else: + index = ensure_int(index, "Index") + if isinstance(target, list): if index < 0 or index >= len(target): raise RuntimeError( @@ -870,6 +1057,17 @@ def _execute_node_field_assign(node: dict): """Handle struct field assignment: obj.field = value""" target_name = node["target"] + if isinstance(target_name, dict): + target = eval_expr_node(target_name) + if isinstance(target, Pointer): + target = target.get() + field_name = node["field"] + value = eval_expr(node["value"]) + if isinstance(target, dict) and field_name in target: + target[field_name] = value + return + raise RuntimeError(f"Object has no field '{field_name}'") + # Check if it's a Python module import alias if target_name in py_imports: # Setting attribute on a Python module @@ -888,11 +1086,14 @@ def _execute_node_field_assign(node: dict): setattr(module, field_name, value) return - if target_name not in vars: + cell = _lookup_var_cell(target_name) + if cell is None: raise RuntimeError(f"Variable '{target_name}' is not defined") - target = vars[target_name]["value"] - target_type = vars[target_name]["type"] + target = cell["value"] + if isinstance(target, Pointer): + target = target.get() + target_type = cell["type"] field_name = node["field"] value = eval_expr(node["value"]) @@ -1101,8 +1302,10 @@ def run_scope(ast: AstType, level=0): # Handle call expressions from AST first (e.g. method calls like obj.method()) # These don't have a 'type' key, just 'func' and 'args' if "func" in node and "args" in node and "type" not in node: - # Execute the expression but discard the result since this is used as a statement - result = eval_expr(node) + # Execute the expression but discard its value. Assigning it to + # `result` would make an if/loop body look like it returned and + # skip the remaining statements in the scope. + eval_expr(node) continue node_type = node["type"] @@ -1131,10 +1334,17 @@ def run_scope(ast: AstType, level=0): _execute_node_index_assign(node) elif node_type == NODE_TYPE_FIELD_ASSIGN: _execute_node_field_assign(node) + elif node_type == "ptr_assign": + target = node.get("target") + if isinstance(target, dict) and target.get("type") == "deref": + target = target.get("value") + pointer = eval_expr_node(target) + if not isinstance(pointer, Pointer): + raise RuntimeError("Cannot assign through a non-pointer value") + pointer.set(eval_expr(node.get("value"))) elif node_type == NODE_TYPE_CALL: - result = run_func(node["name"], node["args"], level) - if len(ast) == 1: - return result + # A bare call statement does not return from the enclosing scope. + run_func(node["name"], node["args"], level) elif node_type == NODE_TYPE_IF: result = _execute_node_if(node) if result is not None: @@ -1160,7 +1370,7 @@ def run_scope(ast: AstType, level=0): elif node_type == NODE_TYPE_ASSERT: _execute_node_assert(node) elif node_type == NODE_TYPE_RETURN: - return eval_expr(node["value"]) + raise ReturnSignal(eval_expr(node["value"])) elif node_type == NODE_TYPE_RAISE: _execute_node_raise(node) else: @@ -1173,7 +1383,7 @@ def run(ast: AstType, file: str = "", source: str = ""): # Use lock to prevent race conditions in parallel test execution with _runtime_lock: global vars, runtime, funcs, py_imports, _current_runtime_vars - global _runtime_file, _runtime_source, _runtime_current_func + global _runtime_file, _runtime_source, _runtime_current_func, _global_vars # Initialize runtime context for error formatting _runtime_file = file @@ -1199,6 +1409,7 @@ def run(ast: AstType, file: str = "", source: str = ""): del funcs[name] run_scope(ast) + _global_vars = vars if "main" not in funcs: raise RuntimeError("Missing main function") @@ -1206,6 +1417,12 @@ def run(ast: AstType, file: str = "", source: str = ""): exit_code = run_func("main", sys.argv[3:]) # If main returns an integer, use it as the exit code if isinstance(exit_code, int): + import builtin_funcs as _builtins + + if getattr(_builtins, "_in_fork_child", False): + sys.stdout.flush() + sys.stderr.flush() + os._exit(int(exit_code)) sys.exit(exit_code) diff --git a/src/wasm_compiler.py b/src/wasm_compiler.py index a657356..ab5c51a 100644 --- a/src/wasm_compiler.py +++ b/src/wasm_compiler.py @@ -57,6 +57,7 @@ def __init__(self): self.local_count = 0 self.label_stack: List[str] = [] # Stack of labels for break/continue self.global_vars: Dict[int, str] = {} # Maps global indices to types + self.global_names: Dict[str, int] = {} # Maps source names to indices self.local_vars: Dict[int, str] = {} # Maps local indices to types self.type_stack: List[str] = [] # Track types on the value stack self.struct_type_stack: List[Optional[int]] = ( @@ -754,6 +755,8 @@ def _parse_bytecode_structure(self, lines: List[str]): fr_type = parts[2] else: fr_type = "i64" + if len(parts) >= 2: + self.global_names[parts[1]] = global_count if fr_type == "str": # Allocate two globals: base for ptr, base+1 for len self.global_vars[global_count] = "i32" @@ -805,8 +808,8 @@ def _parse_bytecode_structure(self, lines: List[str]): # Track referenced globals to backfill missing declarations if line.startswith("LOAD_GLOBAL ") or line.startswith("STORE_GLOBAL "): parts = line.split() - if len(parts) >= 2 and parts[1].isdigit(): - referenced_globals.add(int(parts[1])) + if len(parts) >= 2: + referenced_globals.add(self._global_index(parts[1])) # Ensure we declare globals for any referenced indices not explicitly declared if referenced_globals: @@ -816,6 +819,16 @@ def _parse_bytecode_structure(self, lines: List[str]): # Default to i64 to safely hold packed values self.global_vars[idx] = "i64" + def _global_index(self, token: str) -> int: + if token.isdigit(): + return int(token) + if token in self.global_names: + return self.global_names[token] + idx = max(self.global_vars.keys(), default=-1) + 1 + self.global_names[token] = idx + self.global_vars[idx] = "i64" + return idx + def _map_type_to_wasm(self, fr_type: str) -> str: """Map fr type to WASM type""" if fr_type in ("i32", "i64", "f64"): @@ -5045,7 +5058,7 @@ def push_unpack(t): self.emit(f"local.set {var_ref}_len", indent) elif opcode == "LOAD_GLOBAL": - global_idx = int(args[0]) + global_idx = self._global_index(args[0]) global_ref = f"$g{global_idx}" # Determine type @@ -5073,7 +5086,7 @@ def push_unpack(t): self.extended_type_stack[-1] = "set" elif opcode == "STORE_GLOBAL": - global_idx = int(args[0]) + global_idx = self._global_index(args[0]) global_ref = f"$g{global_idx}" # Determine type