diff --git a/README.md b/README.md index 7712e27..92a0fe1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,6 @@ type errors carry source spans and diagnostic identities; invalid operands propa `lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter. -`interpreter.run` consumes only IR and returns a value or a diagnostic trap; returned strings borrow the module. heap-backed invocation frames track their current block and resume it after calls or global initialization. parameters are initialized on entry and locals execute in statement order; premature local reads trap. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. integers use signed 64-bit storage with checked arithmetic; floats use 64-bit IEEE arithmetic and comparisons, including NaN behavior. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes. +`interpreter.run` consumes only IR and returns a value or a diagnostic trap. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. heap-backed invocation frames track their current block and resume it after calls or global initialization. parameters are initialized on entry and locals execute in statement order; premature local reads trap. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. integers use signed 64-bit storage with checked arithmetic; floats use 64-bit IEEE arithmetic and comparisons, including NaN behavior. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes. recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and conditional arms retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not arms that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation, loops, assignment and boolean logical operators remain unimplemented. diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 9e9c6e4..7c61ff8 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -35,7 +35,7 @@ Frame :: struct { } #! executes only typed IR; invocation and global state are temporary and released on every exit. -#! returned strings borrow `module`; runtime source errors are traps, not infrastructure errors. +#! returned string IDs resolve through `module`; runtime source errors are traps, not infrastructure errors. run proc( allocator mem.Allocator, module @ir.Module, diff --git a/source/interpreter/interpreter.test.hon b/source/interpreter/interpreter.test.hon index 0a5a289..f4cf82a 100644 --- a/source/interpreter/interpreter.test.hon +++ b/source/interpreter/interpreter.test.hon @@ -153,12 +153,18 @@ function_aliases_and_borrowed_string_values_execute test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) text :: "line\n\"quoted\"\x00" + owned :: try mem.alloc(u8, module.allocator, text.len) + transferred := false + defer if (!transferred) mem.free(module.allocator, owned) + memcopy!(owned, text) + try arraylist.append(&module.strings, ir.StringStorage{ allocation = owned, len = text.len }) + transferred = true target :: try add_function(&module, mem.empty(ir.Type), 0, .string, 1) try emit(&module, target, ir.Instruction{ op = .constant, type = .string, destination = ir.ValueId(0), - constant = ir.Value{ type = .string, data = ir.ValueData{ string = text } }, + constant = ir.Value{ type = .string, data = ir.ValueData{ string = ir.StringId(0) } }, }) return_value(&module, target, ir.ValueId(0)) initializer :: try add_function(&module, mem.empty(ir.Type), 0, .function, 1) @@ -173,7 +179,7 @@ function_aliases_and_borrowed_string_values_execute test { outcome :: try run(mem.c_allocator, &module, &store) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.string, outcome.value.type) - try testing.expect_equal([]u8, text, outcome.value.data.string) + try testing.expect_equal([]u8, text, ir.string_bytes(&module, outcome.value.data.string)) } premature_local_reads_and_global_cycles_report_instruction_spans test { diff --git a/source/ir/ir.hon b/source/ir/ir.hon index b18eadb..49a0b33 100644 --- a/source/ir/ir.hon +++ b/source/ir/ir.hon @@ -7,6 +7,7 @@ GlobalId :: distinct u32 LocalId :: distinct u32 ValueId :: distinct u32 BlockId :: distinct u32 +StringId :: distinct u32 no_value :: maxval!(ValueId) Type :: enum { @@ -23,7 +24,7 @@ ValueData :: union { integer i64 real f64 boolean bool - string []u8 + string StringId function FunctionId } Value :: struct { @@ -101,13 +102,19 @@ Global :: struct { span diagnostics.Span } +#! retains the original allocation for freeing while exposing only decoded bytes. +StringStorage :: struct { + allocation []mut u8 + len usize +} + #! owns executable storage, including decoded strings; no AST or binding table is retained. #! spans and diagnostic IDs refer to the caller's source and diagnostic store. Module :: struct { allocator mem.Allocator functions arraylist.ArrayList(Function) globals arraylist.ArrayList(Global) - strings arraylist.ArrayList([]mut u8) + strings arraylist.ArrayList(StringStorage) entry ?FunctionId = null entry_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic } @@ -136,6 +143,12 @@ init_block proc(allocator mem.Allocator) Block { } } +#! resolves a module-local ID to bytes borrowed until module deinit. +string_bytes proc(module @Module, id StringId) []u8 { + string :: module.strings.items[usize(id)] + return string.allocation[..string.len] +} + #! releases all executable storage using the retained allocator. deinit proc(module @mut Module) void { for (0..(module.functions.items.len)) |i| { @@ -149,7 +162,9 @@ deinit proc(module @mut Module) void { arraylist.deinit(&function.locals) arraylist.deinit(&function.blocks) } - for (module.strings.items) |string| mem.free(module.allocator, string) + for (module.strings.items) |string| + mem.free(module.allocator, string.allocation) + arraylist.deinit(&module.functions) arraylist.deinit(&module.globals) arraylist.deinit(&module.strings) diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 25c1e79..c62ce5f 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -560,8 +560,17 @@ lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span) return ir.no_value } - # retain the complete allocation for freeing, but expose only decoded bytes. - try arraylist.append(&state.module.strings, owned) + if (state.module.strings.items.len > usize(maxval!(ir.StringId))) + return .out_of_memory + + string_id :: ir.StringId(state.module.strings.items.len) + try arraylist.append(&state.module.strings, ir.StringStorage{ + allocation = owned, + len = length, + }) transferred = true - return try constant(state, ir.Value{ type = .string, data = ir.ValueData{ string = owned[..length] } }, span) -} + return try constant(state, ir.Value{ + type = .string, + data = ir.ValueData{ string = string_id }, + }, span) +} \ No newline at end of file diff --git a/source/lower/lower.test.hon b/source/lower/lower.test.hon index f05bf90..6e062f5 100644 --- a/source/lower/lower.test.hon +++ b/source/lower/lower.test.hon @@ -91,7 +91,20 @@ owns_decoded_strings_after_source_and_frontend_are_freed test { outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.string, outcome.value.type) - try testing.expect(mem.eql(outcome.value.data.string, "a\\\"\n\r\t\x00Az")) + try testing.expect(mem.eql(ir.string_bytes(&module, outcome.value.data.string), "a\\\"\n\r\t\x00Az")) +} + +resolves_module_local_string_ids_after_frontend_cleanup test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + module := try compile_owned( + "first :: \"not the result\"\nidentity proc(value string) string { return value }\nmain proc() string { return identity(\"second\\n\\x00\") }\n", + &diagnostic_store, + ) + defer ir.deinit(&module) + outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + try testing.expect(outcome.trap == null) + try testing.expect_equal([]u8, "second\n\x00", ir.string_bytes(&module, outcome.value.data.string)) } honors_float_context_and_float_division test { diff --git a/source/main.hon b/source/main.hon index ef1eacc..fed34e7 100644 --- a/source/main.hon +++ b/source/main.hon @@ -133,19 +133,19 @@ run_file proc(allocator mem.Allocator, runtime io.Io, path [;0]u8) i32 ! DriverE module := try compile(allocator, source.items, &diagnostic_store) defer ir.deinit(&module) outcome :: try interpreter.run(allocator, &module, &diagnostic_store) - if (outcome.trap == null) try print_result(io.stdout(runtime), outcome.value) + if (outcome.trap == null) try print_result(io.stdout(runtime), &module, outcome.value) try report_diagnostics(allocator, io.stderr(runtime), path, source.items, &diagnostic_store, outcome.trap) if (outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1 return 0 } @hide:file -print_result proc(output io.Writer, value ir.Value) void ! io.WriteError { +print_result proc(output io.Writer, module @ir.Module, value ir.Value) void ! io.WriteError { match value.type { .int: try io.print("{}\n", output, {value.data.integer}) .float: try io.print("{d}\n", output, {value.data.real}) .bool: try io.print("{}\n", output, {value.data.boolean}) - .string: try io.print("{s}\n", output, {value.data.string}) + .string: try io.print("{s}\n", output, {ir.string_bytes(module, value.data.string)}) .void: {} else: unreachable } diff --git a/source/parser/parse.hon b/source/parser/parse.hon index c40a077..f16bcec 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -408,16 +408,7 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError { arguments arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator) defer arraylist.deinit(&arguments) - while ( - !next_is(state, .close_paren) - and !next_is(state, .newline) - and !next_is(state, .close_curly) - and !next_is(state, .open_curly) - and !next_is(state, .else) - and !next_is(state, .return) - and !next_is(state, .if) - and !next_is(state, .eof) - ) { + while (!is_expression_boundary(current(state).kind)) { argument :: try parse_expr(state) try arraylist.append(&arguments, argument) if (!next_is(state, .comma)) break @@ -482,17 +473,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError { else try diagnostics.add(state.diagnostics, .unexpected_token, token.span) - if ( - token.kind != .newline - and token.kind != .comma - and token.kind != .close_paren - and token.kind != .close_curly - and token.kind != .open_curly - and token.kind != .else - and token.kind != .return - and token.kind != .if - and token.kind != .eof - ) { + if token.kind != .comma and !is_expression_boundary(token.kind) { state.next_token += 1 } @@ -501,6 +482,14 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError { } } +@hide:file +is_expression_boundary proc(kind lexer.TokenKind) bool { + return match kind { + .close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .eof: true + else: false + } +} + @hide:file recover_decl proc( state @mut State,