refactor: name recovery boundaries and compact IR strings
This commit is contained in:
@@ -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.
|
`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.
|
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.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Frame :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#! executes only typed IR; invocation and global state are temporary and released on every exit.
|
#! 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(
|
run proc(
|
||||||
allocator mem.Allocator,
|
allocator mem.Allocator,
|
||||||
module @ir.Module,
|
module @ir.Module,
|
||||||
|
|||||||
@@ -153,12 +153,18 @@ function_aliases_and_borrowed_string_values_execute test {
|
|||||||
store := diagnostics.init(mem.c_allocator)
|
store := diagnostics.init(mem.c_allocator)
|
||||||
defer diagnostics.deinit(&store)
|
defer diagnostics.deinit(&store)
|
||||||
text :: "line\n\"quoted\"\x00"
|
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)
|
target :: try add_function(&module, mem.empty(ir.Type), 0, .string, 1)
|
||||||
try emit(&module, target, ir.Instruction{
|
try emit(&module, target, ir.Instruction{
|
||||||
op = .constant,
|
op = .constant,
|
||||||
type = .string,
|
type = .string,
|
||||||
destination = ir.ValueId(0),
|
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))
|
return_value(&module, target, ir.ValueId(0))
|
||||||
initializer :: try add_function(&module, mem.empty(ir.Type), 0, .function, 1)
|
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)
|
outcome :: try run(mem.c_allocator, &module, &store)
|
||||||
try testing.expect(outcome.trap == null)
|
try testing.expect(outcome.trap == null)
|
||||||
try testing.expect_equal(ir.Type.string, outcome.value.type)
|
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 {
|
premature_local_reads_and_global_cycles_report_instruction_spans test {
|
||||||
|
|||||||
+18
-3
@@ -7,6 +7,7 @@ GlobalId :: distinct u32
|
|||||||
LocalId :: distinct u32
|
LocalId :: distinct u32
|
||||||
ValueId :: distinct u32
|
ValueId :: distinct u32
|
||||||
BlockId :: distinct u32
|
BlockId :: distinct u32
|
||||||
|
StringId :: distinct u32
|
||||||
no_value :: maxval!(ValueId)
|
no_value :: maxval!(ValueId)
|
||||||
|
|
||||||
Type :: enum {
|
Type :: enum {
|
||||||
@@ -23,7 +24,7 @@ ValueData :: union {
|
|||||||
integer i64
|
integer i64
|
||||||
real f64
|
real f64
|
||||||
boolean bool
|
boolean bool
|
||||||
string []u8
|
string StringId
|
||||||
function FunctionId
|
function FunctionId
|
||||||
}
|
}
|
||||||
Value :: struct {
|
Value :: struct {
|
||||||
@@ -101,13 +102,19 @@ Global :: struct {
|
|||||||
span diagnostics.Span
|
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.
|
#! 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.
|
#! spans and diagnostic IDs refer to the caller's source and diagnostic store.
|
||||||
Module :: struct {
|
Module :: struct {
|
||||||
allocator mem.Allocator
|
allocator mem.Allocator
|
||||||
functions arraylist.ArrayList(Function)
|
functions arraylist.ArrayList(Function)
|
||||||
globals arraylist.ArrayList(Global)
|
globals arraylist.ArrayList(Global)
|
||||||
strings arraylist.ArrayList([]mut u8)
|
strings arraylist.ArrayList(StringStorage)
|
||||||
entry ?FunctionId = null
|
entry ?FunctionId = null
|
||||||
entry_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
|
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.
|
#! releases all executable storage using the retained allocator.
|
||||||
deinit proc(module @mut Module) void {
|
deinit proc(module @mut Module) void {
|
||||||
for (0..(module.functions.items.len)) |i| {
|
for (0..(module.functions.items.len)) |i| {
|
||||||
@@ -149,7 +162,9 @@ deinit proc(module @mut Module) void {
|
|||||||
arraylist.deinit(&function.locals)
|
arraylist.deinit(&function.locals)
|
||||||
arraylist.deinit(&function.blocks)
|
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.functions)
|
||||||
arraylist.deinit(&module.globals)
|
arraylist.deinit(&module.globals)
|
||||||
arraylist.deinit(&module.strings)
|
arraylist.deinit(&module.strings)
|
||||||
|
|||||||
+12
-3
@@ -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)
|
trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span)
|
||||||
return ir.no_value
|
return ir.no_value
|
||||||
}
|
}
|
||||||
# retain the complete allocation for freeing, but expose only decoded bytes.
|
if (state.module.strings.items.len > usize(maxval!(ir.StringId)))
|
||||||
try arraylist.append(&state.module.strings, owned)
|
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
|
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)
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,20 @@ owns_decoded_strings_after_source_and_frontend_are_freed test {
|
|||||||
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
outcome :: try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
try testing.expect(outcome.trap == null)
|
try testing.expect(outcome.trap == null)
|
||||||
try testing.expect_equal(ir.Type.string, outcome.value.type)
|
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 {
|
honors_float_context_and_float_division test {
|
||||||
|
|||||||
+3
-3
@@ -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)
|
module := try compile(allocator, source.items, &diagnostic_store)
|
||||||
defer ir.deinit(&module)
|
defer ir.deinit(&module)
|
||||||
outcome :: try interpreter.run(allocator, &module, &diagnostic_store)
|
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)
|
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
|
if (outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@hide:file
|
@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 {
|
match value.type {
|
||||||
.int: try io.print("{}\n", output, {value.data.integer})
|
.int: try io.print("{}\n", output, {value.data.integer})
|
||||||
.float: try io.print("{d}\n", output, {value.data.real})
|
.float: try io.print("{d}\n", output, {value.data.real})
|
||||||
.bool: try io.print("{}\n", output, {value.data.boolean})
|
.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: {}
|
.void: {}
|
||||||
else: unreachable
|
else: unreachable
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-21
@@ -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)
|
arguments arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
||||||
defer arraylist.deinit(&arguments)
|
defer arraylist.deinit(&arguments)
|
||||||
|
|
||||||
while (
|
while (!is_expression_boundary(current(state).kind)) {
|
||||||
!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)
|
|
||||||
) {
|
|
||||||
argument :: try parse_expr(state)
|
argument :: try parse_expr(state)
|
||||||
try arraylist.append(&arguments, argument)
|
try arraylist.append(&arguments, argument)
|
||||||
if (!next_is(state, .comma)) break
|
if (!next_is(state, .comma)) break
|
||||||
@@ -482,17 +473,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
|
|||||||
else
|
else
|
||||||
try diagnostics.add(state.diagnostics, .unexpected_token, token.span)
|
try diagnostics.add(state.diagnostics, .unexpected_token, token.span)
|
||||||
|
|
||||||
if (
|
if token.kind != .comma and !is_expression_boundary(token.kind) {
|
||||||
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
|
|
||||||
) {
|
|
||||||
state.next_token += 1
|
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
|
@hide:file
|
||||||
recover_decl proc(
|
recover_decl proc(
|
||||||
state @mut State,
|
state @mut State,
|
||||||
|
|||||||
Reference in New Issue
Block a user